Skip to content

Commit 449e79d

Browse files
authored
feat(rest-api): add disable/enable action endpoints for hosts (#9532)
1 parent 9d657ac commit 449e79d

4 files changed

Lines changed: 107 additions & 1 deletion

File tree

@vates/types/src/lib/xen-orchestra-xapi.mts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
WrappedXenApiRecord,
33
XenApiHost,
4+
XenApiHostWrapped,
45
XenApiNetwork,
56
XenApiNetworkWrapped,
67
XenApiRecord,
@@ -315,6 +316,16 @@ export interface Xapi {
315316
pathname: string,
316317
params?: { host?: XenApiHost; query?: Record<string, unknown>; task?: boolean | XenApiTask['$ref'] }
317318
): Promise<{ body: Readable }>
319+
clearHost(host: Pick<XenApiHostWrapped, '$ref' | '$pool'>, force?: boolean): Promise<void>
320+
disableHost(hostId: XoHost['id']): Promise<void>
321+
enableHost(hostId: XoHost['id']): Promise<void>
322+
getRecordByUuid<
323+
Type extends WrappedXenApiRecord['$type'],
324+
XenApiRecord extends WrappedXenApiRecord = Extract<WrappedXenApiRecord, { $type: Type }>,
325+
>(
326+
type: Type,
327+
uuid: XenApiRecord['uuid']
328+
): Promise<XenApiRecord>
318329
isHyperThreadingEnabled(hostId: XoHost['id']): Promise<boolean | null>
319330
VTPM_create(params: { VM: XenApiVm['$ref']; is_unique?: boolean; contents?: string }): Promise<XenApiVtpm['$ref']>
320331
}

@vates/types/src/xen-api.mts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ type TagCallMethods = {
8282
type WrapperXenApi<T, Type extends string, Fn = { (): void }> = T & {
8383
$call: Fn
8484
$callAsync: Fn
85+
$pool: XenApiPool
8586
$type: Type
8687
$snapshot(params: {
8788
cancelToken?: unknown

@xen-orchestra/rest-api/src/hosts/host.controller.mts

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import {
1515
SuccessResponse,
1616
Tags,
1717
} from 'tsoa'
18+
import { asyncEach } from '@vates/async-each'
19+
import { type Defer, defer } from 'golike-defer'
1820
import { json } from 'express'
1921
import type { Request as ExRequest, Response as ExResponse } from 'express'
2022
import { inject } from 'inversify'
@@ -30,6 +32,7 @@ import type {
3032
XoMessage,
3133
XoPif,
3234
XoTask,
35+
XoVm,
3336
XsPatches,
3437
} from '@vates/types'
3538

@@ -345,7 +348,97 @@ export class HostController extends XapiXoController<XoHost> {
345348
taskProperties: {
346349
name: 'reconfigure host management interface',
347350
objectId: hostId,
348-
args: body,
351+
params: body,
352+
},
353+
})
354+
}
355+
356+
/**
357+
* Disable a host.
358+
*
359+
* Set `evacuate` to `true` to also evacuate all running VMs to other hosts in the pool.
360+
*
361+
* Use `vmIdsToForceMigrate` to unblock VMs whose migration is currently blocked (e.g. by `pool_migrate` or `migrate_send` blocked operations).
362+
*
363+
* Use `force` to ignore evacuation errors.
364+
*
365+
* @example id "b61a5c92-700e-4966-a13b-00633f03eea8"
366+
* @example body { "evacuate": true, "vmIdsToForceMigrate": ["f07ab729-c0e8-721c-45ec-f11276377030"] }
367+
*/
368+
@Example(taskLocation)
369+
@Post('{id}/actions/disable')
370+
@Middlewares(json())
371+
@SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description)
372+
@Response(noContentResp.status, noContentResp.description)
373+
@Response(notFoundResp.status, notFoundResp.description)
374+
@Response(invalidParametersResp.status, invalidParametersResp.description)
375+
@Response(internalServerErrorResp.status, internalServerErrorResp.description)
376+
disable(
377+
@Path() id: string,
378+
// mark `evacuate` as optional to workaround a TSOA issue. See https://github.com/lukeautry/tsoa/pull/1840
379+
@Body() body?: { evacuate?: false } | { evacuate: true; force?: boolean; vmIdsToForceMigrate?: string[] },
380+
@Query() sync?: boolean
381+
): CreateActionReturnType<void> {
382+
const hostId = id as XoHost['id']
383+
const action = defer(async ($defer: Defer) => {
384+
const xapiHost = this.getXapiObject(hostId)
385+
const xapi = xapiHost.$xapi
386+
387+
if (body?.evacuate !== true) {
388+
await xapi.call('host.disable', xapiHost.$ref)
389+
return
390+
}
391+
392+
if (body.vmIdsToForceMigrate !== undefined) {
393+
await asyncEach(body.vmIdsToForceMigrate, async vmId => {
394+
const xoVm = this.restApi.getObject<XoVm>(vmId as XoVm['id'], 'VM')
395+
for (const operation of ['pool_migrate', 'migrate_send'] as const) {
396+
const reason = xoVm.blockedOperations[operation]
397+
if (reason !== undefined) {
398+
await xapi.call('VM.remove_from_blocked_operations', xoVm._xapiRef, operation)
399+
$defer(() => xapi.call('VM.add_to_blocked_operations', xoVm._xapiRef, operation, reason))
400+
}
401+
}
402+
})
403+
}
404+
405+
await xapi.clearHost(xapiHost, body.force)
406+
})
407+
408+
return this.createAction<void>(action, {
409+
sync,
410+
statusCode: noContentResp.status,
411+
taskProperties: {
412+
name: body?.evacuate === true ? 'disable and evacuate host' : 'disable host',
413+
objectId: hostId,
414+
params: body,
415+
},
416+
})
417+
}
418+
419+
/**
420+
* Enable a host, taking it out of disabled state.
421+
*
422+
* @example id "b61a5c92-700e-4966-a13b-00633f03eea8"
423+
*/
424+
@Example(taskLocation)
425+
@Post('{id}/actions/enable')
426+
@SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description)
427+
@Response(noContentResp.status, noContentResp.description)
428+
@Response(notFoundResp.status, notFoundResp.description)
429+
@Response(internalServerErrorResp.status, internalServerErrorResp.description)
430+
enable(@Path() id: string, @Query() sync?: boolean): CreateActionReturnType<void> {
431+
const hostId = id as XoHost['id']
432+
const action = async () => {
433+
await this.getXapiObject(hostId).$xapi.enableHost(hostId)
434+
}
435+
436+
return this.createAction<void>(action, {
437+
sync,
438+
statusCode: noContentResp.status,
439+
taskProperties: {
440+
name: 'enable host',
441+
objectId: hostId,
349442
},
350443
})
351444
}

CHANGELOG.unreleased.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
- [S3] add configuration for max/minPartSize and maxPartNumber in the API (PR [#9561](https://github.com/vatesfr/xen-orchestra/pull/9561))
1717
- [REST API] Expose `/rest/v0/vms/:id/actions/clone` (PR [#9453](https://github.com/vatesfr/xen-orchestra/pull/9453))
1818
- [REST API] Expose POST `/rest/v0/srs/:id/actions/forget` (PR [#9505](https://github.com/vatesfr/xen-orchestra/pull/9505))
19+
- [REST API] Add `POST /hosts/{id}/actions/disable` and `POST /hosts/{id}/actions/enable` endpoints (PR [#9532](https://github.com/vatesfr/xen-orchestra/pull/9532))
1920

2021
### Bug fixes
2122

0 commit comments

Comments
 (0)