feat(xo-server,xo-web/rpu): shut down and restart pinned VMs instead of aborting (#10125)

* feat(xo-server/rpu): opt-in shutdown/restart of pinned VMs instead of aborting
This commit is contained in:
Olivier Lambert
2026-07-28 10:05:43 +02:00
committed by GitHub
parent 8d58147d66
commit 4ca263c10a
13 changed files with 275 additions and 37 deletions

View File

@@ -340,8 +340,11 @@ export type XoApp = {
readOnly?: XoServer['readOnly']
}
): Promise<XoServer>
rollingPoolReboot(pool: XoPool, opts?: { parentTask?: VatesTask }): Promise<void>
rollingPoolUpdate(pool: XoPool, opts?: { rebootVm?: boolean; parentTask?: VatesTask }): Promise<void>
rollingPoolReboot(pool: XoPool, opts?: { parentTask?: VatesTask; shutdownPinnedVms?: boolean }): Promise<void>
rollingPoolUpdate(
pool: XoPool,
opts?: { rebootVm?: boolean; parentTask?: VatesTask; shutdownPinnedVms?: boolean }
): Promise<void>
setVmResourceSet(vmId: XoVm['id'], resourceSetId: string | null, force?: boolean): Promise<void>
shareVmResourceSet(vmId: XoVm['id']): Promise<void>
removeUserFromGroup(userId: XoUser['id'], id: XoGroup['id']): Promise<void>

View File

@@ -31,6 +31,7 @@ import {
createdResp,
featureUnauthorized,
forbiddenOperationResp,
incorrectStateResp,
internalServerErrorResp,
invalidParameters as invalidParametersResp,
noContentResp,
@@ -359,22 +360,33 @@ export class PoolController extends XapiXoController<XoPool> {
* Required privilege:
* - resource: pool, action: rolling-reboot
*
* Set `shutdownPinnedVms` to `true` to shut down VMs that cannot be migrated (PCI passthrough, vGPU, SR-IOV VIF)
* before their host reboots and start them again on it afterwards. Without it, such VMs make the action fail
* with an `incorrect state` error listing their UUIDs.
*
* @example id "355ee47d-ff4c-4924-3db2-fd86ae629677"
* @example body { "shutdownPinnedVms": true }
*/
@Example(taskLocation)
@Extension('x-mcp-exposure', 'confirm')
@Post('{id}/actions/rolling_reboot')
@Middlewares(acl({ resource: 'pool', action: 'rolling-reboot', objectId: 'params.id' }))
@Middlewares([json(), acl({ resource: 'pool', action: 'rolling-reboot', objectId: 'params.id' })])
@SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description)
@Response(forbiddenOperationResp.status, forbiddenOperationResp.description)
@Response(noContentResp.status, noContentResp.description)
@Response(featureUnauthorized.status, featureUnauthorized.description)
@Response(notFoundResp.status, notFoundResp.description)
rollingReboot(@Path() id: string, @Query() sync?: boolean): CreateActionReturnType<void> {
@Response(incorrectStateResp.status, incorrectStateResp.description)
rollingReboot(
@Path() id: string,
@Body() body?: { shutdownPinnedVms?: boolean },
@Query() sync?: boolean
): CreateActionReturnType<void> {
const poolId = id as XoPool['id']
const shutdownPinnedVms = body?.shutdownPinnedVms ?? false
const action = async (task: VatesTask) => {
const pool = this.getObject(poolId)
await this.restApi.xoApp.rollingPoolReboot(pool, { parentTask: task })
await this.restApi.xoApp.rollingPoolReboot(pool, { parentTask: task, shutdownPinnedVms })
}
return this.createAction<void>(action, {
@@ -392,22 +404,33 @@ export class PoolController extends XapiXoController<XoPool> {
* Required privilege:
* - resource: pool, action: rolling-update
*
* Set `shutdownPinnedVms` to `true` to shut down VMs that cannot be migrated (PCI passthrough, vGPU, SR-IOV VIF)
* before their host reboots and start them again on it afterwards. Without it, such VMs make the action fail
* with an `incorrect state` error listing their UUIDs.
*
* @example id "355ee47d-ff4c-4924-3db2-fd86ae629677"
* @example body { "shutdownPinnedVms": true }
*/
@Example(taskLocation)
@Extension('x-mcp-exposure', 'confirm')
@Post('{id}/actions/rolling_update')
@Middlewares(acl({ resource: 'pool', action: 'rolling-update', objectId: 'params.id' }))
@Middlewares([json(), acl({ resource: 'pool', action: 'rolling-update', objectId: 'params.id' })])
@SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description)
@Response(forbiddenOperationResp.status, forbiddenOperationResp.description)
@Response(noContentResp.status, noContentResp.description)
@Response(featureUnauthorized.status, featureUnauthorized.description)
@Response(notFoundResp.status, notFoundResp.description)
rollingUpdate(@Path() id: string, @Query() sync?: boolean): CreateActionReturnType<void> {
@Response(incorrectStateResp.status, incorrectStateResp.description)
rollingUpdate(
@Path() id: string,
@Body() body?: { shutdownPinnedVms?: boolean },
@Query() sync?: boolean
): CreateActionReturnType<void> {
const poolId = id as XoPool['id']
const shutdownPinnedVms = body?.shutdownPinnedVms ?? false
const action = async (task: VatesTask) => {
const pool = this.getObject(poolId)
await this.restApi.xoApp.rollingPoolUpdate(pool, { parentTask: task })
await this.restApi.xoApp.rollingPoolUpdate(pool, { parentTask: task, shutdownPinnedVms })
}
return this.createAction<void>(action, {

View File

@@ -12,6 +12,8 @@
> Users must be able to say: "Nice enhancement, I'm eager to test it"
- [XO6/Treeview] Fix hosts alignment in Treeview when hosts have different numbers of VMs (PR [#10153](https://github.com/vatesfr/xen-orchestra/pull/10153))
- [REST API] Possibility of sending `shutdownPinnedVms` in the body of the `/pools/:id/actions/rolling_update` and `rolling_reboot` endpoints (PR [#10125](https://github.com/vatesfr/xen-orchestra/pull/10125))
- [Rolling Pool Update/Reboot] New `shutdownPinnedVms` option: VMs that cannot be migrated because they use a host-bound device (PCI passthrough, vGPU, SR-IOV VIF) are cleanly shut down before their host reboots and started again on it afterwards, instead of aborting the whole run. When such VMs block the run, XO now lists them and asks for confirmation instead of failing with a raw `CANNOT_EVACUATE_HOST` error (PR [#10125](https://github.com/vatesfr/xen-orchestra/pull/10125))
### Bug fixes
@@ -41,10 +43,14 @@
<!--packages-start-->
- @vates/types minor
- @xen-orchestra/backup-archive patch
- @xen-orchestra/backups patch
- @xen-orchestra/disk-transform patch
- @xen-orchestra/rest-api minor
- @xen-orchestra/web minor
- @xen-orchestra/web-core minor
- xo-server minor
- xo-web minor
<!--packages-end-->

View File

@@ -242,6 +242,8 @@ vdiDelayBeforeRemovingCloudConfigDrive = '5 min'
vdiExportConcurrency = 12
vmEvacuationConcurrency = 3
vmExportConcurrency = 2
# The duration XO will wait for a VM to shut down cleanly before forcing it
vmShutdownTimeout = '10 minutes'
vmSnapshotConcurrency = 2
poolMarkingInterval = '6 hours'

View File

@@ -58,7 +58,7 @@ Old traces are garbage-collected on mtime (`rpu.tracesRetention`, 31 days by def
| Timeout on `Waiting for host to be up` | Host takes too long to boot. `xapiOptions.restartHostTimeout` (default 20 minutes). |
| Pool stays `disconnected` after the master rebooted, `EHOSTUNREACH` | Stale connection error, the retry did not kick in yet. `POST /rest/v0/servers/<id>/actions/connect` reconnects immediately. |
Note on granularity: `Evacuate` is a single `host.evacuate` XAPI call, there is no per-VM detail in the tree for that phase. Per-VM subtasks only exist in `Migrate VMs back`.
Note on granularity: `Evacuate` is a single `host.evacuate` XAPI call, there is no per-VM detail in the tree for that phase. When `shutdownPinnedVms` is enabled and pinned VMs are present, per-VM subtasks also appear under `Shut down pinned VMs` and `Restart pinned VMs`.
## Task logs
@@ -70,12 +70,20 @@ Rolling pool update and rolling pool reboot task logs have major parts in common
task.start({ name: 'Rolling pool reboot', poolId: string, poolName: string })
├─ task.start({ name: 'Restarting hosts', total: number, progress: number, done: number })
| ├─ task.start({ name: `Restarting host ${hostId}`, hostId: string, hostName: string })
| | ├─ task.start({ name: 'Shut down pinned VMs', hostId: string, hostName: string })
| | | ├─ task.start({ name: `Shutting down VM ${vmId}`, hostId: string, hostName: string, vmId: string, vmName: string })
│ │ │ │ └─ task.end
│ │ │ └─ task.end
| | ├─ task.start({ name: 'Evacuate', hostId: string, hostName: string })
│ │ │ └─ task.end
| | ├─ task.start({ name: 'Restart', hostId: string, hostName: string })
│ │ │ └─ task.end
| | ├─ task.start({ name: 'Waiting for host to be up', hostId: string, hostName: string })
│ │ │ └─ task.end
| | ├─ task.start({ name: 'Restart pinned VMs', hostId: string, hostName: string })
| | | ├─ task.start({ name: `Restarting VM ${vmId} on host ${hostId}`, hostId: string, hostName: string, vmId: string, vmName: string })
│ │ │ │ └─ task.end
│ │ │ └─ task.end
│ │ └─ task.end
│ └─ task.end
├─ task.start({ name: 'Migrate VMs back' })
@@ -100,6 +108,10 @@ task.start({ name: 'Rolling pool update', poolId: string, poolName: string })
│ │ └─ task.end
│ ├─ task.start({ name: 'Restarting hosts', total: number, progress: number, done: number })
│ | ├─ task.start({ name: `Restarting host ${hostId}`, hostId: string, hostName: string })
│ | | ├─ task.start({ name: 'Shut down pinned VMs', hostId: string, hostName: string })
│ | | | ├─ task.start({ name: `Shutting down VM ${vmId}`, hostId: string, hostName: string, vmId: string, vmName: string })
│ │ │ │ │ └─ task.end
│ │ │ │ └─ task.end
│ | | ├─ task.start({ name: 'Evacuate', hostId: string, hostName: string })
│ │ │ │ └─ task.end
│ | | ├─ task.start({ name: 'Installing patches', hostId: string, hostName: string })
@@ -108,6 +120,10 @@ task.start({ name: 'Rolling pool update', poolId: string, poolName: string })
│ │ │ │ └─ task.end
│ | | ├─ task.start({ name: 'Waiting for host to be up', hostId: string, hostName: string })
│ │ │ │ └─ task.end
│ | | ├─ task.start({ name: 'Restart pinned VMs', hostId: string, hostName: string })
│ | | | ├─ task.start({ name: `Restarting VM ${vmId} on host ${hostId}`, hostId: string, hostName: string, vmId: string, vmName: string })
│ │ │ │ │ └─ task.end
│ │ │ │ └─ task.end
│ │ │ └─ task.end
│ │ └─ task.end
│ ├─ task.start({ name: 'Migrate VMs back' })

View File

@@ -242,7 +242,7 @@ installPatches.description = 'Install patches on hosts'
// -------------------------------------------------------------------
export const rollingUpdate = async function ({ bypassBackupCheck = false, pool, rebootVm }) {
export const rollingUpdate = async function ({ bypassBackupCheck = false, pool, rebootVm, shutdownPinnedVms }) {
const poolId = pool.id
if (bypassBackupCheck) {
log.warn('pool.rollingUpdate with argument "bypassBackupCheck" set to true', { poolId })
@@ -250,7 +250,7 @@ export const rollingUpdate = async function ({ bypassBackupCheck = false, pool,
await backupGuard.call(this, poolId)
}
await this.rollingPoolUpdate(pool, { rebootVm })
await this.rollingPoolUpdate(pool, { rebootVm, shutdownPinnedVms })
}
rollingUpdate.params = {
@@ -263,6 +263,12 @@ rollingUpdate.params = {
optional: true,
type: 'boolean',
},
// shut down VMs that cannot be migrated (PCI passthrough, vGPU, SR-IOV VIF)
// before their host reboots and start them again on it afterwards
shutdownPinnedVms: {
default: false,
type: 'boolean',
},
}
rollingUpdate.resolve = {
@@ -271,7 +277,7 @@ rollingUpdate.resolve = {
// -------------------------------------------------------------------
export async function rollingReboot({ bypassBackupCheck, pool }) {
export async function rollingReboot({ bypassBackupCheck, pool, shutdownPinnedVms }) {
const poolId = pool.id
if (bypassBackupCheck) {
log.warn('pool.rollingReboot with argument "bypassBackupCheck" set to true', { poolId })
@@ -279,7 +285,7 @@ export async function rollingReboot({ bypassBackupCheck, pool }) {
await backupGuard.call(this, poolId)
}
await this.rollingPoolReboot(pool)
await this.rollingPoolReboot(pool, { shutdownPinnedVms })
}
rollingReboot.params = {
@@ -288,6 +294,12 @@ rollingReboot.params = {
type: 'boolean',
},
pool: { type: 'string' },
// shut down VMs that cannot be migrated (PCI passthrough, vGPU, SR-IOV VIF)
// before their host reboots and start them again on it afterwards
shutdownPinnedVms: {
default: false,
type: 'boolean',
},
}
rollingReboot.resolve = {

View File

@@ -78,6 +78,7 @@ export default class Xapi extends XapiBase {
vmEvacuationConcurrency,
vmExportConcurrency,
vmMigrationConcurrency = 3,
vmShutdownTimeout,
vmSnapshotConcurrency,
...opts
}) {
@@ -87,6 +88,7 @@ export default class Xapi extends XapiBase {
this._maxUncoalescedVdis = maxUncoalescedVdis
this._restartHostTimeout = parseDuration(restartHostTimeout)
this._vmEvacuationConcurrency = vmEvacuationConcurrency
this._vmShutdownTimeout = parseDuration(vmShutdownTimeout)
// close event is emitted when the export is canceled via browser. See https://github.com/vatesfr/xen-orchestra/issues/5535
const waitStreamEnd = async stream => fromEvents(await stream, ['end', 'close'])

View File

@@ -726,7 +726,11 @@ const methods = {
})
},
async rollingPoolUpdate($defer, parentTask, { xsCredentials, force = false, rebootVm = force } = {}) {
async rollingPoolUpdate(
$defer,
parentTask,
{ xsCredentials, force = false, rebootVm = force, shutdownPinnedVms = false } = {}
) {
if (some(this.objects.indexes.type.SR, { type: 'linstor' })) {
await this._updateLinstorPackages()
}
@@ -790,6 +794,7 @@ const methods = {
await Task.run({ properties: { name: `Updating and rebooting` } }, async () => {
await this.rollingPoolReboot(parentTask, {
xsCredentials,
shutdownPinnedVms,
beforeEvacuateVms: () => {
// On XS < 8.4 and CH, start by installing patches on all hosts
if (!isXcp && !isXsWithCdnUpdates) {

View File

@@ -1,3 +1,4 @@
import { asyncEach } from '@vates/async-each'
import { cancelable, timeout } from 'promise-toolbox'
import { createLogger } from '@xen-orchestra/log'
import { decorateObject } from '@vates/decorate-with'
@@ -14,6 +15,18 @@ const log = createLogger('xo:xapi')
const PATH_DB_DUMP = '/pool/xmldbdump'
// XAPI error codes identifying VMs that can never be evacuated because they use
// a host-bound device (PCI passthrough, vGPU, SR-IOV VIF): these VMs can only
// be handled by shutting them down before their host reboots and starting them
// again on it afterwards
const PINNED_VM_ERROR_CODES = new Set(['VM_HAS_PCI_ATTACHED', 'VM_HAS_VGPU', 'VM_HAS_SRIOV_VIF'])
// pinned VMs are shut down in parallel to keep the host's downtime short, but
// started back more conservatively to avoid a boot storm on a host which has
// just rebooted
const PINNED_VM_SHUTDOWN_CONCURRENCY = 8
const PINNED_VM_START_CONCURRENCY = 2
const setProgress = (task, progress) => task.set('progress', Math.round(progress))
const methods = {
@@ -35,7 +48,11 @@ const methods = {
})
},
async rollingPoolReboot($defer, parentTask, { beforeEvacuateVms, beforeRebootHost, ignoreHost } = {}) {
async rollingPoolReboot(
$defer,
parentTask,
{ beforeEvacuateVms, beforeRebootHost, ignoreHost, shutdownPinnedVms = false } = {}
) {
if (this.pool.ha_enabled) {
const haSrs = this.pool.$ha_statefiles.map(vdi => vdi.SR)
const haConfig = this.pool.ha_configuration
@@ -64,9 +81,60 @@ const methods = {
}
}
// when shutdownPinnedVms is enabled, pinned VMs will be shut down before
// their host reboots and started again on it afterwards, otherwise their
// UUIDs are collected to raise a single actionable error covering the
// whole pool, any other evacuation blocker aborts the run
//
// this check requires HA to be already disabled: with HA enabled, XAPI
// reports every non-protected VM as an evacuation blocker
const unhandledPinnedVmUuids = []
await Promise.all(
hosts.filter(host => !ignoreHost || !ignoreHost(host)).map(host => host.$call('assert_can_evacuate'))
hosts
.filter(host => !ignoreHost || !ignoreHost(host))
.map(async host => {
const blockedVms = await host.$call('get_vms_which_prevent_evacuation')
const vmRefs = Object.keys(blockedVms)
if (vmRefs.length === 0) {
return
}
const canHandleAllBlockers = Object.values(blockedVms).every(([errorCode]) =>
PINNED_VM_ERROR_CODES.has(errorCode)
)
if (!canHandleAllBlockers) {
// let XAPI raise its canonical CANNOT_EVACUATE_HOST error
return host.$call('assert_can_evacuate')
}
if (!shutdownPinnedVms) {
unhandledPinnedVmUuids.push(...vmRefs.map(vmRef => this.getObject(vmRef).uuid))
}
})
)
if (unhandledPinnedVmUuids.length > 0) {
// the run can proceed if the caller consents to shut these VMs down
// during their host's reboot, by enabling shutdownPinnedVms
throw incorrectState({
actual: unhandledPinnedVmUuids,
expected: [],
object: this.pool.uuid,
property: 'pinnedVms',
})
}
// VMs shut down for their host's reboot and not started again yet: if the
// run aborts, leave them running rather than halted
const haltedPinnedVms = new Map() // VM ref -> host ref
$defer(async () => {
for (const [vmRef, hostRef] of haltedPinnedVms) {
try {
await this.callAsync('VM.start_on', vmRef, hostRef, false, false)
} catch (error) {
log.warn('failed to restart pinned VM after an aborted rolling pool reboot', { vmRef, error })
}
}
})
// Steps in the RPR : Evacuate hosts, reboot hosts, migrate VMs back, and potentially updateHosts (beforeEvacuateVms and beforeRebootHost)
const nSteps = 3 + Number(beforeEvacuateVms !== undefined) + Number(beforeRebootHost !== undefined)
@@ -130,6 +198,42 @@ const methods = {
const getServerTime = async () => parseDateTime(await this.call('host.get_servertime', host.$ref)) * 1e3
let pinnedVmRefs = []
if (shutdownPinnedVms) {
// fresh query instead of reusing the initial check: the pool
// state may have changed while handling the previous hosts
const blockedVms = await host.$call('get_vms_which_prevent_evacuation')
pinnedVmRefs = Object.entries(blockedVms)
.filter(([, [errorCode]]) => PINNED_VM_ERROR_CODES.has(errorCode))
.map(([vmRef]) => vmRef)
if (pinnedVmRefs.length > 0) {
await Task.run({ properties: { name: `Shut down pinned VMs`, hostId, hostName } }, async () => {
await asyncEach(
pinnedVmRefs,
async vmRef => {
const { uuid: vmId, name_label: vmName } = this.getObject(vmRef)
await Task.run(
{ properties: { name: `Shutting down VM ${vmId}`, hostId, hostName, vmId, vmName } },
async () => {
try {
// a guest may ignore the shutdown request: cancel it and force the shutdown
// instead of blocking the whole run, the user consented to these VMs going down
await timeout.call(this.callAsync('VM.clean_shutdown', vmRef), this._vmShutdownTimeout)
} catch (error) {
log.warn('clean shutdown of a pinned VM failed, forcing it', { vmId, error })
await this.callAsync('VM.hard_shutdown', vmRef)
}
}
)
haltedPinnedVms.set(vmRef, host.$ref)
},
{ concurrency: PINNED_VM_SHUTDOWN_CONCURRENCY, stopOnError: true }
)
})
}
}
// the pool state may have changed since the initial check, e.g. while evacuating the previous hosts
await Task.run({ properties: { name: `Check evacuation precondition`, hostId, hostName } }, async () => {
await host.$call('assert_can_evacuate')
@@ -194,6 +298,26 @@ const methods = {
new Error(`Host ${hostId} took too long to restart`)
)
})
if (pinnedVmRefs.length > 0) {
await Task.run({ properties: { name: `Restart pinned VMs`, hostId, hostName } }, async () => {
// stopOnError: still try to start every pinned VM of this host before failing the run
await asyncEach(
pinnedVmRefs,
async vmRef => {
const { uuid: vmId, name_label: vmName } = this.getObject(vmRef)
await Task.run(
{
properties: { name: `Restarting VM ${vmId} on host ${hostId}`, hostId, hostName, vmId, vmName },
},
() => this.callAsync('VM.start_on', vmRef, host.$ref, false, false)
)
haltedPinnedVms.delete(vmRef)
},
{ concurrency: PINNED_VM_START_CONCURRENCY, stopOnError: false }
)
})
}
rprProgress += progressStepPerHost
setProgress(parentTask, rprProgress)
subtaskProgress += subtaskProgressStep

View File

@@ -205,7 +205,7 @@ export default class Pools {
)
}
async rollingPoolReboot(pool, { parentTask } = {}) {
async rollingPoolReboot(pool, { parentTask, shutdownPinnedVms } = {}) {
const { _app } = this
await _app.checkFeatureAuthorization('ROLLING_POOL_REBOOT')
const releaseGuard = acquireRpuGuard(pool.id, 'rollingPoolReboot')
@@ -226,7 +226,7 @@ export default class Pools {
}
const task = parentTask === undefined ? _app.tasks.create(properties) : new Task({ properties })
trace?.attach(task)
await task.run(async () => _app.getXapi(pool).rollingPoolReboot(task))
await task.run(async () => _app.getXapi(pool).rollingPoolReboot(task, { shutdownPinnedVms }))
} finally {
trace?.stop()
releaseGuard()

View File

@@ -900,7 +900,7 @@ export default class XenServers {
})
}
async rollingPoolUpdate($defer, pool, { rebootVm, parentTask } = {}) {
async rollingPoolUpdate($defer, pool, { rebootVm, parentTask, shutdownPinnedVms } = {}) {
const app = this._app
await app.checkFeatureAuthorization('ROLLING_POOL_UPDATE')
const [schedules, jobs] = await Promise.all([app.getAllSchedules(), app.getAllJobs('backup')])
@@ -973,6 +973,7 @@ export default class XenServers {
this.getXapi(pool).rollingPoolUpdate(task, {
xsCredentials: app.apiContext.user.preferences.xsCredentials,
rebootVm,
shutdownPinnedVms,
})
)
}

View File

@@ -2689,6 +2689,8 @@ const messages = {
replicationCountHigherThanHostsWithDisks: 'Replication count is higher than number of hosts with disks',
resourceList: 'Resource list',
rpuRequireVmsReboot: 'To fully apply the patches, some VMs will reboot. Are you sure you want to continue?',
rpuShutdownPinnedVms:
'The following VMs use a host-bound device (PCI passthrough, vGPU, SR-IOV VIFs) and cannot be migrated. They will be shut down before their host reboots and started again on it afterwards. Are you sure you want to continue?',
selectDisks: 'Select disk(s)…',
selectedDiskTypeIncompatibleXostor: 'Only disks of type "Disk" and "Raid" are accepted. Selected disk type: {type}.',
setAsPreferred: 'Set as preferred',

View File

@@ -998,23 +998,45 @@ export const rollingPoolReboot = async pool => {
title: _('rollingPoolReboot'),
icon: 'pool-rolling-reboot',
})
try {
return await _call('pool.rollingReboot', { pool: poolId })
} catch (error) {
if (!forbiddenOperation.is(error)) {
const rpr = async ({ bypassBackupCheck = false, shutdownPinnedVms = false } = {}) => {
try {
return await _call('pool.rollingReboot', { pool: poolId, bypassBackupCheck, shutdownPinnedVms })
} catch (error) {
if (forbiddenOperation.is(error)) {
await confirm({
body: (
<p className='text-warning'>
<Icon icon='alarm' /> {_('bypassBackupPoolModalMessage')}
</p>
),
title: _('rollingPoolReboot'),
icon: 'pool-rolling-reboot',
})
return rpr({ bypassBackupCheck: true, shutdownPinnedVms })
}
if (incorrectState.is(error, { property: 'pinnedVms' })) {
await confirm({
body: (
<div className='text-warning'>
<p>
<Icon icon='alarm' /> {_('rpuShutdownPinnedVms')}
</p>
<ul>
{error.data.actual.map(vmId => (
<li key={vmId}>{renderXoItemFromId(vmId)}</li>
))}
</ul>
</div>
),
title: _('rollingPoolReboot'),
icon: 'pool-rolling-reboot',
})
return rpr({ bypassBackupCheck, shutdownPinnedVms: true })
}
throw error
}
await confirm({
body: (
<p className='text-warning'>
<Icon icon='alarm' /> {_('bypassBackupPoolModalMessage')}
</p>
),
title: _('rollingPoolReboot'),
icon: 'pool-rolling-reboot',
})
return _call('pool.rollingReboot', { pool: poolId, bypassBackupCheck: true })
}
return rpr()
}
export const getPoolGuestSecureBootReadiness = async poolId => {
@@ -1433,9 +1455,9 @@ export const rollingPoolUpdate = async poolId => {
icon: 'pool-rolling-update',
})
const rpu = async ({ bypassBackupCheck = false, rebootVm = false } = {}) => {
const rpu = async ({ bypassBackupCheck = false, rebootVm = false, shutdownPinnedVms = false } = {}) => {
try {
await _call('pool.rollingUpdate', { pool: poolId, bypassBackupCheck, rebootVm })
await _call('pool.rollingUpdate', { pool: poolId, bypassBackupCheck, rebootVm, shutdownPinnedVms })
subscribeHostMissingPatches.forceRefresh()
} catch (err) {
if (forbiddenOperation.is(err)) {
@@ -1448,7 +1470,7 @@ export const rollingPoolUpdate = async poolId => {
title: _('rollingPoolUpdate'),
icon: 'pool-rolling-update',
})
await rpu({ bypassBackupCheck: true, rebootVm })
return rpu({ bypassBackupCheck: true, rebootVm, shutdownPinnedVms })
}
if (incorrectState.is(err, { property: 'guidance' })) {
await confirm({
@@ -1460,8 +1482,28 @@ export const rollingPoolUpdate = async poolId => {
title: _('rollingPoolUpdate'),
icon: 'pool-rolling-update',
})
await rpu({ bypassBackupCheck, rebootVm: true })
return rpu({ bypassBackupCheck, rebootVm: true, shutdownPinnedVms })
}
if (incorrectState.is(err, { property: 'pinnedVms' })) {
await confirm({
body: (
<div className='text-warning'>
<p>
<Icon icon='alarm' /> {_('rpuShutdownPinnedVms')}
</p>
<ul>
{err.data.actual.map(vmId => (
<li key={vmId}>{renderXoItemFromId(vmId)}</li>
))}
</ul>
</div>
),
title: _('rollingPoolUpdate'),
icon: 'pool-rolling-update',
})
return rpu({ bypassBackupCheck, rebootVm, shutdownPinnedVms: true })
}
throw err
}
}