feat(@xen-orchestra/rest-api): migrate vms/:id/actions/start (#8379)

This commit is contained in:
Mathieu
2025-03-11 12:50:42 +01:00
committed by GitHub
parent 57056b2298
commit ca84af4eda
16 changed files with 221 additions and 81 deletions

View File

@@ -13,6 +13,10 @@
"./common": {
"default": "./dist/common.mjs",
"types": "./dist/common.d.mts"
},
"./lib/vates/*": {
"default": "./dist/lib/vates-*.mjs",
"type": "./dist/lib/vates-*.d.mts"
}
},
"type": "module",

View File

@@ -0,0 +1,7 @@
import { Branded } from '../common.mjs'
export type Task = {
id: Branded<'task'>
run: <T>(fn: () => T) => Promise<T>
}

View File

@@ -68,9 +68,22 @@ import type {
// https://github.com/vatesfr/xen-orchestra/tree/wip-xapi-types-generator/%40xen-orchestra/xapi-generator
// All properties added after XenServer 7.0 (dundee) release have been marked as optional
/**
* Add properties injected by `xen-api`.
* $ref property is also injected by XOLite, so she is not present here.
*/
type WrapperXenApi<T, Type extends string, Fn = { (): void }> = T & {
$call: Fn
$callAsync: Fn
$type: Type
$xapi: {
call: <ReturnType>(...args: unknown[]) => Promise<ReturnType>
callAsync: <ReturnType>(...args: unknown[]) => Promise<ReturnType>
}
}
export interface XenApiSession {
$ref: Branded<'session'>
$type: 'session'
auth_user_name: string
auth_user_sid: string
client_certificate?: boolean
@@ -91,12 +104,10 @@ export interface XenApiSession {
export interface XenApiAuth {
$ref: Branded<'auth'>
$type: 'auth'
}
export interface XenApiSubject {
$ref: Branded<'subject'>
$type: 'subject'
other_config: Record<string, string>
roles: XenApiRole['$ref'][]
subject_identifier: string
@@ -105,7 +116,6 @@ export interface XenApiSubject {
export interface XenApiRole {
$ref: Branded<'role'>
$type: 'role'
is_internal?: boolean
name_description: string
name_label: string
@@ -115,7 +125,6 @@ export interface XenApiRole {
export interface XenApiTask {
$ref: Branded<'task'>
$type: 'task'
allowed_operations: TASK_ALLOWED_OPERATIONS[]
backtrace: string
created?: string
@@ -137,7 +146,6 @@ export interface XenApiTask {
export interface XenApiEvent {
$ref: Branded<'event'>
$type: 'event'
class?: string
id?: number
/** @deprecated */
@@ -151,7 +159,6 @@ export interface XenApiEvent {
export interface XenApiPool {
$ref: Branded<'pool'>
$type: 'pool'
allowed_operations: POOL_ALLOWED_OPERATIONS[]
blobs: Record<string, XenApiBlob['$ref']>
client_certificate_auth_enabled?: boolean
@@ -212,10 +219,10 @@ export interface XenApiPool {
/** @deprecated */
wlb_verify_cert?: boolean
}
export type XenApiPoolWrapped = WrapperXenApi<XenApiPool, 'pool'>
export interface XenApiPoolPatch {
$ref: Branded<'pool_patch'>
$type: 'pool_patch'
after_apply_guidance: AFTER_APPLY_GUIDANCE[]
host_patches: XenApiHostPatch['$ref'][]
name_description: string
@@ -230,7 +237,6 @@ export interface XenApiPoolPatch {
export interface XenApiPoolUpdate {
$ref: Branded<'pool_update'>
$type: 'pool_update'
after_apply_guidance?: UPDATE_AFTER_APPLY_GUIDANCE[]
enforce_homogeneity?: boolean
hosts?: XenApiHost['$ref'][]
@@ -244,9 +250,11 @@ export interface XenApiPoolUpdate {
version?: string
}
type XenApiVmCallMethods = {
(method: 'start', start_paused: boolean, force: boolean): Promise<void>
}
export interface XenApiVm {
$ref: Branded<'VM'>
$type: 'VM'
actions_after_crash?: ON_CRASH_BEHAVIOUR
actions_after_reboot?: ON_NORMAL_EXIT
actions_after_shutdown?: ON_NORMAL_EXIT
@@ -344,10 +352,10 @@ export interface XenApiVm {
VUSBs: XenApiVusb['$ref'][]
xenstore_data: Record<string, string>
}
export type XenApiVmWrapped = WrapperXenApi<XenApiVm, 'VM', XenApiVmCallMethods>
export interface XenApiVmMetrics {
$ref: Branded<'VM_metrics'>
$type: 'VM_metrics'
current_domain_type?: DOMAIN_TYPE
hvm?: boolean
install_time?: string
@@ -369,7 +377,6 @@ export interface XenApiVmMetrics {
export interface XenApiVmGuestMetrics {
$ref: Branded<'VM_guest_metrics'>
$type: 'VM_guest_metrics'
can_use_hotplug_vbd: TRISTATE_TYPE
can_use_hotplug_vif: TRISTATE_TYPE
/** @deprecated */
@@ -391,7 +398,6 @@ export interface XenApiVmGuestMetrics {
export interface XenApiVmpp {
$ref: Branded<'VMPP'>
$type: 'VMPP'
/** @deprecated */
alarm_config?: Record<string, string>
/** @deprecated */
@@ -434,7 +440,6 @@ export interface XenApiVmpp {
export interface XenApiVmss {
$ref: Branded<'VMSS'>
$type: 'VMSS'
enabled: boolean
frequency?: VMSS_FREQUENCY
last_run_time: string
@@ -449,7 +454,6 @@ export interface XenApiVmss {
export interface XenApiVmAppliance {
$ref: Branded<'VM_appliance'>
$type: 'VM_appliance'
allowed_operations: VM_APPLIANCE_OPERATION[]
current_operations: Record<string, VM_APPLIANCE_OPERATION>
name_description: string
@@ -460,14 +464,12 @@ export interface XenApiVmAppliance {
export interface XenApiDrTask {
$ref: Branded<'DR_task'>
$type: 'DR_task'
introduced_SRs: XenApiSr['$ref'][]
uuid: string
}
export interface XenApiHost {
$ref: Branded<'host'>
$type: 'host'
address?: string
allowed_operations: HOST_ALLOWED_OPERATIONS[]
API_version_major?: number
@@ -542,10 +544,10 @@ export interface XenApiHost {
uuid: string
virtual_hardware_platform_versions: number[]
}
export type XenApiHostWrapped = WrapperXenApi<XenApiHost, 'host'>
export interface XenApiHostCrashdump {
$ref: Branded<'host_crashdump'>
$type: 'host_crashdump'
host?: XenApiHost['$ref']
other_config: Record<string, string>
size?: number
@@ -555,7 +557,6 @@ export interface XenApiHostCrashdump {
export interface XenApiHostPatch {
$ref: Branded<'host_patch'>
$type: 'host_patch'
applied?: boolean
host?: XenApiHost['$ref']
name_description: string
@@ -570,7 +571,6 @@ export interface XenApiHostPatch {
export interface XenApiHostMetrics {
$ref: Branded<'host_metrics'>
$type: 'host_metrics'
last_updated?: string
live?: boolean
/** @deprecated */
@@ -582,7 +582,6 @@ export interface XenApiHostMetrics {
export interface XenApiHostCpu {
$ref: Branded<'host_cpu'>
$type: 'host_cpu'
family?: number
features?: string
flags?: string
@@ -600,7 +599,6 @@ export interface XenApiHostCpu {
export interface XenApiNetwork {
$ref: Branded<'network'>
$type: 'network'
allowed_operations: NETWORK_OPERATIONS[]
assigned_ips: Record<XenApiVif['$ref'], string>
blobs: Record<string, XenApiBlob['$ref']>
@@ -621,7 +619,6 @@ export interface XenApiNetwork {
export interface XenApiVif {
$ref: Branded<'VIF'>
$type: 'VIF'
allowed_operations: VIF_OPERATIONS[]
current_operations: Record<string, VIF_OPERATIONS>
currently_attached: boolean
@@ -651,10 +648,10 @@ export interface XenApiVif {
uuid: string
VM?: XenApiVm['$ref']
}
export type XenApiVifWrapped = WrapperXenApi<XenApiVif, 'VIF'>
export interface XenApiVifMetrics {
$ref: Branded<'VIF_metrics'>
$type: 'VIF_metrics'
/** @deprecated */
io_read_kbs?: number
/** @deprecated */
@@ -666,7 +663,6 @@ export interface XenApiVifMetrics {
export interface XenApiPif {
$ref: Branded<'PIF'>
$type: 'PIF'
bond_master_of: XenApiBond['$ref'][]
bond_slave_of?: XenApiBond['$ref']
capabilities: string[]
@@ -706,7 +702,6 @@ export interface XenApiPif {
export interface XenApiPifMetrics {
$ref: Branded<'PIF_metrics'>
$type: 'PIF_metrics'
carrier?: boolean
device_id?: string
device_name?: string
@@ -726,7 +721,6 @@ export interface XenApiPifMetrics {
export interface XenApiBond {
$ref: Branded<'Bond'>
$type: 'Bond'
auto_update_mac?: boolean
links_up: number
master?: XenApiPif['$ref']
@@ -740,7 +734,6 @@ export interface XenApiBond {
export interface XenApiVlan {
$ref: Branded<'VLAN'>
$type: 'VLAN'
other_config: Record<string, string>
tag: number
tagged_PIF?: XenApiPif['$ref']
@@ -750,7 +743,6 @@ export interface XenApiVlan {
export interface XenApiSm {
$ref: Branded<'SM'>
$type: 'SM'
/** @deprecated */
capabilities?: string[]
configuration?: Record<string, string>
@@ -770,7 +762,6 @@ export interface XenApiSm {
export interface XenApiSr {
$ref: Branded<'SR'>
$type: 'SR'
allowed_operations: STORAGE_OPERATIONS[]
blobs: Record<string, XenApiBlob['$ref']>
clustered: boolean
@@ -793,10 +784,10 @@ export interface XenApiSr {
VDIs: XenApiVdi['$ref'][]
virtual_allocation?: number
}
export type XenApiSrWrapped = WrapperXenApi<XenApiSr, 'SR'>
export interface XenApiSrStat {
$ref: Branded<'sr_stat'>
$type: 'sr_stat'
clustered?: boolean
free_space?: number
health?: SR_HEALTH
@@ -808,7 +799,6 @@ export interface XenApiSrStat {
export interface XenApiProbeResult {
$ref: Branded<'probe_result'>
$type: 'probe_result'
complete?: boolean
configuration?: Record<string, string>
extra_info?: Record<string, string>
@@ -817,13 +807,11 @@ export interface XenApiProbeResult {
export interface XenApiLvhd {
$ref: Branded<'LVHD'>
$type: 'LVHD'
uuid: string
}
export interface XenApiVdi {
$ref: Branded<'VDI'>
$type: 'VDI'
allow_caching: boolean
allowed_operations: VDI_OPERATIONS[]
cbt_enabled?: boolean
@@ -858,10 +846,10 @@ export interface XenApiVdi {
virtual_size?: number
xenstore_data: Record<string, string>
}
export type XenApiVdiWrapped = WrapperXenApi<XenApiVdi, 'VDI'>
export interface XenApiVbd {
$ref: Branded<'VBD'>
$type: 'VBD'
allowed_operations: VBD_OPERATIONS[]
bootable?: boolean
current_operations: Record<string, VBD_OPERATIONS>
@@ -886,10 +874,10 @@ export interface XenApiVbd {
VDI?: XenApiVdi['$ref']
VM?: XenApiVm['$ref']
}
export type XenApiVbdWrapped = WrapperXenApi<XenApiVbd, 'VBD'>
export interface XenApiVbdMetrics {
$ref: Branded<'VBD_metrics'>
$type: 'VBD_metrics'
/** @deprecated */
io_read_kbs?: number
/** @deprecated */
@@ -903,7 +891,6 @@ export interface XenApiVbdMetrics {
export interface XenApiPbd {
$ref: Branded<'PBD'>
$type: 'PBD'
currently_attached?: boolean
device_config?: Record<string, string>
host?: XenApiHost['$ref']
@@ -914,7 +901,6 @@ export interface XenApiPbd {
export interface XenApiCrashdump {
$ref: Branded<'crashdump'>
$type: 'crashdump'
other_config: Record<string, string>
uuid: string
VDI?: XenApiVdi['$ref']
@@ -923,7 +909,6 @@ export interface XenApiCrashdump {
export interface XenApiVtpm {
$ref: Branded<'VTPM'>
$type: 'VTPM'
allowed_operations: VTPM_OPERATIONS[]
backend?: XenApiVm['$ref']
current_operations: Record<string, VTPM_OPERATIONS>
@@ -933,10 +918,10 @@ export interface XenApiVtpm {
uuid: string
VM?: XenApiVm['$ref']
}
export type XenApiVtpmWrapped = WrapperXenApi<XenApiVtpm, 'VTPM'>
export interface XenApiConsole {
$ref: Branded<'console'>
$type: 'console'
location?: string
other_config?: Record<string, string>
protocol?: CONSOLE_PROTOCOL
@@ -946,7 +931,6 @@ export interface XenApiConsole {
export interface XenApiUser {
$ref: Branded<'user'>
$type: 'user'
fullname?: string
other_config: Record<string, string>
short_name?: string
@@ -955,7 +939,6 @@ export interface XenApiUser {
export interface XenApiDataSource {
$ref: Branded<'data_source'>
$type: 'data_source'
enabled?: boolean
max?: number
min?: number
@@ -968,7 +951,6 @@ export interface XenApiDataSource {
export interface XenApiBlob {
$ref: Branded<'blob'>
$type: 'blob'
last_updated?: string
mime_type?: string
name_description: string
@@ -980,7 +962,6 @@ export interface XenApiBlob {
export interface XenApiMessage {
$ref: Branded<'message'>
$type: 'message'
body?: string
cls?: CLS
name?: string
@@ -992,7 +973,6 @@ export interface XenApiMessage {
export interface XenApiSecret {
$ref: Branded<'secret'>
$type: 'secret'
other_config: Record<string, string>
uuid: string
value?: string
@@ -1000,7 +980,6 @@ export interface XenApiSecret {
export interface XenApiTunnel {
$ref: Branded<'tunnel'>
$type: 'tunnel'
access_PIF?: XenApiPif['$ref']
other_config: Record<string, string>
protocol?: TUNNEL_PROTOCOL
@@ -1011,7 +990,6 @@ export interface XenApiTunnel {
export interface XenApiNetworkSriov {
$ref: Branded<'network_sriov'>
$type: 'network_sriov'
configuration_mode?: SRIOV_CONFIGURATION_MODE
logical_PIF?: XenApiPif['$ref']
physical_PIF?: XenApiPif['$ref']
@@ -1021,7 +999,6 @@ export interface XenApiNetworkSriov {
export interface XenApiPci {
$ref: Branded<'PCI'>
$type: 'PCI'
class_name: string
dependencies: XenApiPci['$ref'][]
device_name: string
@@ -1037,7 +1014,6 @@ export interface XenApiPci {
export interface XenApiPgpu {
$ref: Branded<'PGPU'>
$type: 'PGPU'
compatibility_metadata?: Record<string, string>
dom0_access: PGPU_DOM0_ACCESS
enabled_VGPU_types: XenApiVgpuType['$ref'][]
@@ -1054,7 +1030,6 @@ export interface XenApiPgpu {
export interface XenApiGpuGroup {
$ref: Branded<'GPU_group'>
$type: 'GPU_group'
allocation_algorithm: ALLOCATION_ALGORITHM
enabled_VGPU_types: XenApiVgpuType['$ref'][]
GPU_types: string[]
@@ -1069,7 +1044,6 @@ export interface XenApiGpuGroup {
export interface XenApiVgpu {
$ref: Branded<'VGPU'>
$type: 'VGPU'
compatibility_metadata?: Record<string, string>
currently_attached: boolean
device: string
@@ -1083,10 +1057,10 @@ export interface XenApiVgpu {
uuid: string
VM?: XenApiVm['$ref']
}
export type XenApiVgpuWrapped = WrapperXenApi<XenApiVgpu, 'VGPU'>
export interface XenApiVgpuType {
$ref: Branded<'VGPU_type'>
$type: 'VGPU_type'
compatible_types_in_vm?: XenApiVgpuType['$ref'][]
enabled_on_GPU_groups: XenApiGpuGroup['$ref'][]
enabled_on_PGPUs: XenApiPgpu['$ref'][]
@@ -1107,7 +1081,6 @@ export interface XenApiVgpuType {
export interface XenApiPvsSite {
$ref: Branded<'PVS_site'>
$type: 'PVS_site'
cache_storage?: XenApiPvsCacheStorage['$ref'][]
name_description?: string
name_label?: string
@@ -1119,7 +1092,6 @@ export interface XenApiPvsSite {
export interface XenApiPvsServer {
$ref: Branded<'PVS_server'>
$type: 'PVS_server'
addresses?: string[]
first_port?: number
last_port?: number
@@ -1129,7 +1101,6 @@ export interface XenApiPvsServer {
export interface XenApiPvsProxy {
$ref: Branded<'PVS_proxy'>
$type: 'PVS_proxy'
currently_attached?: boolean
site?: XenApiPvsSite['$ref']
status?: PVS_PROXY_STATUS
@@ -1139,7 +1110,6 @@ export interface XenApiPvsProxy {
export interface XenApiPvsCacheStorage {
$ref: Branded<'PVS_cache_storage'>
$type: 'PVS_cache_storage'
host?: XenApiHost['$ref']
site?: XenApiPvsSite['$ref']
size?: number
@@ -1150,7 +1120,6 @@ export interface XenApiPvsCacheStorage {
export interface XenApiFeature {
$ref: Branded<'Feature'>
$type: 'Feature'
enabled?: boolean
experimental?: boolean
host?: XenApiHost['$ref']
@@ -1162,7 +1131,6 @@ export interface XenApiFeature {
export interface XenApiSdnController {
$ref: Branded<'SDN_controller'>
$type: 'SDN_controller'
address?: string
port?: number
protocol?: SDN_CONTROLLER_PROTOCOL
@@ -1171,7 +1139,6 @@ export interface XenApiSdnController {
export interface XenApiVdiNbdServerInfo {
$ref: Branded<'vdi_nbd_server_info'>
$type: 'vdi_nbd_server_info'
address?: string
cert?: string
exportname?: string
@@ -1181,7 +1148,6 @@ export interface XenApiVdiNbdServerInfo {
export interface XenApiPusb {
$ref: Branded<'PUSB'>
$type: 'PUSB'
description?: string
host?: XenApiHost['$ref']
other_config?: Record<string, string>
@@ -1200,7 +1166,6 @@ export interface XenApiPusb {
export interface XenApiUsbGroup {
$ref: Branded<'USB_group'>
$type: 'USB_group'
name_description?: string
name_label?: string
other_config?: Record<string, string>
@@ -1211,7 +1176,6 @@ export interface XenApiUsbGroup {
export interface XenApiVusb {
$ref: Branded<'VUSB'>
$type: 'VUSB'
allowed_operations: VUSB_OPERATIONS[]
current_operations: Record<string, VUSB_OPERATIONS>
currently_attached: boolean
@@ -1223,7 +1187,6 @@ export interface XenApiVusb {
export interface XenApiCluster {
$ref: Branded<'Cluster'>
$type: 'Cluster'
allowed_operations: CLUSTER_OPERATION[]
cluster_config?: Record<string, string>
cluster_hosts?: XenApiClusterHost['$ref'][]
@@ -1243,7 +1206,6 @@ export interface XenApiCluster {
export interface XenApiClusterHost {
$ref: Branded<'Cluster_host'>
$type: 'Cluster_host'
allowed_operations: CLUSTER_HOST_OPERATION[]
cluster?: XenApiCluster['$ref']
current_operations: Record<string, CLUSTER_HOST_OPERATION>
@@ -1259,7 +1221,6 @@ export interface XenApiClusterHost {
export interface XenApiCertificate {
$ref: Branded<'Certificate'>
$type: 'Certificate'
fingerprint?: string
host?: XenApiHost['$ref']
name?: string
@@ -1271,7 +1232,6 @@ export interface XenApiCertificate {
export interface XenApiRepository {
$ref: Branded<'Repository'>
$type: 'Repository'
binary_url?: string
gpgkey_path?: string
hash?: string
@@ -1286,7 +1246,6 @@ export interface XenApiRepository {
export interface XenApiObserver {
$ref: Branded<'Observer'>
$type: 'Observer'
attributes?: Record<string, string>
components?: string[]
enabled?: boolean
@@ -1365,3 +1324,14 @@ export type XenApiRecord =
| XenApiCertificate
| XenApiRepository
| XenApiObserver
export type WrappedXenApiRecord =
| XenApiHostWrapped
| XenApiPoolWrapped
| XenApiSrWrapped
| XenApiVbdWrapped
| XenApiVdiWrapped
| XenApiVgpuWrapped
| XenApiVifWrapped
| XenApiVmWrapped
| XenApiVtpmWrapped

View File

@@ -30,6 +30,7 @@ class Foo extends Controller {
@Example(['foo', 'bar'])
@Get('{id}')
@Security('*')
@SuccessResponse(202)
@Response(404)
getFoo(@Path() id: string) {
return this.getFoo(id)

View File

@@ -48,6 +48,7 @@ class Foo extends Controller {
@Example(['foo', 'bar'])
@Get('{id}')
@Security('*')
@SuccessResponse(202)
@Response(404)
getFoo(@Path() id: string) {
return this.getFoo(id)

View File

@@ -1,12 +1,15 @@
import * as CM from 'complex-matcher'
import { Controller } from 'tsoa'
import { Controller, HttpStatusCodeLiteral } from 'tsoa'
import { Request } from 'express'
import type { XapiXoRecord } from '@vates/types/xo'
import { BASE_URL } from '../index.mjs'
import { RestApi } from '../rest-api/rest-api.mjs'
import { makeObjectMapper } from '../helpers/object-wrapper.helper.mjs'
import type { WithHref } from '../helpers/helper.type.mjs'
const noop = () => {}
export abstract class XapiXoController<T extends XapiXoRecord> extends Controller {
#type: T['type']
restApi: RestApi
@@ -34,4 +37,43 @@ export abstract class XapiXoController<T extends XapiXoRecord> extends Controlle
return mappedObjects
}
getXapiObject(maybeId: T['id'] | T) {
return this.restApi.getXapiObject<T>(maybeId, this.#type)
}
/**
* statusCode must represent the status code in case of a synchronous request. Default 200
*/
async createAction<CbType>(
cb: () => CbType,
{
statusCode = 200,
sync = false,
taskProperties,
}: {
statusCode?: HttpStatusCodeLiteral
sync?: boolean
taskProperties: { name: string; objectId: T['id']; args?: Record<string, unknown>; [key: string]: unknown }
}
) {
taskProperties.name = 'REST API: ' + taskProperties.name
taskProperties.type = 'xo:rest-api:action'
const task = this.restApi.tasks.create(taskProperties)
const pResult = task.run(cb)
if (sync) {
this.setStatus(statusCode)
return pResult
} else {
pResult.catch(noop)
const location = `${BASE_URL}/tasks/${task.id}`
this.setStatus(202)
this.setHeader('Location', location)
this.setHeader('Content-Type', 'text/plain')
return location
}
}
}

View File

@@ -8,10 +8,10 @@ import type { WithHref } from './helper.type.mjs'
const { join } = path.posix
export function makeObjectMapper<T extends XapiXoRecord>(req: Request, path = req.path) {
const makeUrl = ({ id }: T) => join(baseUrl, path, typeof id === 'number' ? String(id) : id)
const makeUrl = ({ id }: T) => join(path, typeof id === 'number' ? String(id) : id)
let objectMapper: (object: T) => string | WithHref<Partial<T>> | WithHref<T>
const { query, baseUrl } = req
const { query } = req
const { fields } = query
if (fields === '*') {
objectMapper = object => ({

View File

@@ -13,6 +13,8 @@ import type { XoApp } from './rest-api/rest-api.type.mjs'
const require = createRequire(import.meta.url)
const swaggerOpenApiSpec = require('../open-api/spec/swagger.json')
export const BASE_URL = '/rest/v0'
export default function setupRestApi(express: Express, xoApp: XoApp) {
setupContainer(xoApp)
RegisterRoutes(express)
@@ -20,8 +22,8 @@ export default function setupRestApi(express: Express, xoApp: XoApp) {
// do not register the doc at the root level, or it may lead to unwated behaviour
// uncomment when all endpoints are migrated to this API
// express.get('/rest/v0', (_req, res) => res.redirect('/rest/v0/docs'))
express.use('/rest/v0/docs', swaggerUi.serve, swaggerUi.setup(swaggerOpenApiSpec))
express.use(`${BASE_URL}/docs`, swaggerUi.serve, swaggerUi.setup(swaggerOpenApiSpec))
express.use('/rest/v0', tsoaToXoErrorHandler)
express.use('/rest/v0', genericErrorHandler)
express.use(BASE_URL, tsoaToXoErrorHandler)
express.use(BASE_URL, genericErrorHandler)
}

View File

@@ -21,6 +21,7 @@ export default function genericErrorHandler(error: unknown, req: Request, res: R
return
}
const responseError: { error: string; info?: string } = { error: error.message }
if (noSuchObject.is(error)) {
res.status(404)
} else if (unauthorized.is(error) || forbiddenOperation.is(error) || featureUnauthorized.is(error)) {
@@ -32,11 +33,14 @@ export default function genericErrorHandler(error: unknown, req: Request, res: R
} else if (notImplemented.is(error)) {
res.status(501)
} else {
if (error.name === 'XapiError') {
responseError.info = 'This is a XenServer/XCP-ng error, not an XO error'
}
res.status(500)
log.error(error)
}
log.info(`[${req.method}] ${req.path} (${res.statusCode})`)
res.json({ error: error.message })
res.json(responseError)
}

View File

@@ -13,6 +13,12 @@ export type Unbrand<T> = {
: T[K]
}
export const actionAsyncroneResp = {
status: 202,
description: 'Action executed asynchronously',
produce: 'text/plain',
} as const
export const unauthorizedResp = {
status: 401,
description: 'Authentication required',
@@ -23,6 +29,11 @@ export const notFoundResp = {
description: 'Resource not found',
} as const
export const noContentResp = {
status: 204,
description: 'No content',
} as const
export const internalServerErrorResp = {
status: 500,
description: 'Internal server error, XenServer/XCP-ng error',

View File

@@ -0,0 +1 @@
export const taskLocation = '/rest/v0/tasks/0m7kl0j9l'

View File

@@ -9,6 +9,10 @@ export class RestApi {
this.#xoApp = xoApp
}
get tasks() {
return this.#xoApp.tasks
}
get xoApp() {
return this.#xoApp
}
@@ -25,6 +29,10 @@ export class RestApi {
return this.#xoApp.getObjectsByType(type, opts)
}
getXapiObject<T extends XapiXoRecord>(maybeId: T['id'] | T, type: T['type']) {
return this.#xoApp.getXapiObject<T>(maybeId, type)
}
runWithApiContext(...args: Parameters<XoApp['runWithApiContext']>) {
return this.#xoApp.runWithApiContext(...args)
}

View File

@@ -1,7 +1,40 @@
import type { EventEmitter } from 'node:events'
import type { Task } from '@vates/types/lib/vates/task'
import type { XapiHostStats, XapiVmStats, XapiStatsGranularity } from '@vates/types/common'
import type {
XenApiHostWrapped,
XenApiPoolWrapped,
XenApiSrWrapped,
XenApiVbdWrapped,
XenApiVdiWrapped,
XenApiVgpuWrapped,
XenApiVifWrapped,
XenApiVmWrapped,
XenApiVtpmWrapped,
} from '@vates/types/xen-api'
import type { XoHost, XoUser, XapiXoRecord, XoVm } from '@vates/types/xo'
type XapiRecordByXapiXoRecord = {
host: XenApiHostWrapped
pool: XenApiPoolWrapped
SR: XenApiSrWrapped
VBD: XenApiVbdWrapped
VDI: XenApiVdiWrapped
VGPU: XenApiVgpuWrapped
VIF: XenApiVifWrapped
VM: XenApiVmWrapped
'VM-controller': XenApiVmWrapped
'VM-snapshot': XenApiVmWrapped
'VM-template': XenApiVmWrapped
VTPM: XenApiVtpmWrapped
}
export type XoApp = {
tasks: EventEmitter & {
create: (params: { name: string; objectId?: string; type?: string }) => Task
}
// methods ------------
authenticateUser: (
credentials: { token?: string; username?: string; password?: string },
userData?: { ip?: string },
@@ -13,6 +46,7 @@ export type XoApp = {
opts?: { filter?: string; limit?: number }
) => Record<T['id'], T>
getXapiHostStats: (hostId: XoHost['id'], granularity?: XapiStatsGranularity) => Promise<XapiHostStats>
getXapiObject: <T extends XapiXoRecord>(maybeId: T['id'] | T, type: T['type']) => XapiRecordByXapiXoRecord[T['type']]
getXapiVmStats: (vmId: XoVm['id'], granularity?: XapiStatsGranularity) => Promise<XapiVmStats>
runWithApiContext: (user: XoUser, fn: () => void) => Promise<unknown>
}

View File

@@ -1,4 +1,17 @@
import { Example, Get, Path, Queries, Query, Request, Response, Route, Security, Tags } from 'tsoa'
import {
Example,
Get,
Path,
Post,
Queries,
Query,
Request,
Response,
Route,
Security,
Tags,
SuccessResponse,
} from 'tsoa'
import { Request as ExRequest } from 'express'
import { inject } from 'inversify'
import { incorrectState, invalidParameters } from 'xo-common/api-errors.js'
@@ -6,9 +19,17 @@ import { provide } from 'inversify-binding-decorators'
import type { XapiStatsGranularity, XapiVmStats, XoVm } from '@vates/types'
import { CollectionQueryParams } from '../open-api/common/request.common.mjs'
import { notFoundResp, unauthorizedResp, type Unbrand } from '../open-api/common/response.common.mjs'
import {
actionAsyncroneResp,
internalServerErrorResp,
noContentResp,
notFoundResp,
unauthorizedResp,
type Unbrand,
} from '../open-api/common/response.common.mjs'
import { partialVms, vm, vmIds, vmStatsExample } from '../open-api/oa-examples/vm.oa-example.mjs'
import { RestApi } from '../rest-api/rest-api.mjs'
import { taskLocation } from '../open-api/oa-examples/task.oa-example.mjs'
import type { WithHref } from '../helpers/helper.type.mjs'
import { XapiXoController } from '../abstract-classes/xapi-xo-controller.mjs'
@@ -76,4 +97,27 @@ export class VmController extends XapiXoController<XoVm> {
throw error
}
}
/**
* @example id "f07ab729-c0e8-721c-45ec-f11276377030"
*/
@Example(taskLocation)
@Post('{id}/actions/start')
@SuccessResponse(actionAsyncroneResp.status, actionAsyncroneResp.description, actionAsyncroneResp.produce)
@Response(noContentResp.status, noContentResp.description)
@Response(notFoundResp.status, notFoundResp.description)
@Response(internalServerErrorResp.status, internalServerErrorResp.description)
async startVm(@Path() id: string, @Query() sync?: boolean) {
const vmId = id as XoVm['id']
const action = () => this.getXapiObject(vmId).$callAsync('start', false, false)
return this.createAction(action, {
sync,
statusCode: noContentResp.status,
taskProperties: {
name: 'start VM',
objectId: vmId,
},
})
}
}

View File

@@ -13,6 +13,7 @@
- **Migrated REST API endpoints**:
- `/rest/v0/vms/<vm-id>/stats` (PR [#8359](https://github.com/vatesfr/xen-orchestra/pull/8359))
- `/rest/v0/vms/<vm-id>/actions/start` (PR [#8379](https://github.com/vatesfr/xen-orchestra/pull/8379))
- `/rest/v0/hosts` (PR [#8372](https://github.com/vatesfr/xen-orchestra/pull/8372))
- `/rest/v0/hosts/<host-id>` (PR [#8372](https://github.com/vatesfr/xen-orchestra/pull/8372))
- `/rest/v0/hosts/<host-id>/stats` (PR [#8372](https://github.com/vatesfr/xen-orchestra/pull/8372))

View File

@@ -605,7 +605,16 @@ export default class RestApi {
const collections = { __proto__: null }
// add migrated collections to maintain their discoverability
const swaggerEndpoints = ['docs', 'vms', 'hosts', 'srs']
const swaggerEndpoints = {
docs: {},
vms: {
actions: {
start: true,
},
},
hosts: {},
srs: {},
}
const withParams = (fn, paramsSchema) => {
fn.params = paramsSchema
@@ -1146,7 +1155,7 @@ export default class RestApi {
api.get(
'/',
wrap((req, res) => {
const endpoints = new Set([...Object.keys(collections), ...swaggerEndpoints])
const endpoints = new Set([...Object.keys(collections), ...Object.keys(swaggerEndpoints)])
return sendObjects(endpoints, req, res)
})
)
@@ -1283,7 +1292,7 @@ export default class RestApi {
'/:collection',
wrap(async (req, res, next) => {
const { collection, query } = req
if (swaggerEndpoints.includes(collection.id)) {
if (swaggerEndpoints[collection.id] !== undefined) {
return next('route')
}
@@ -1358,7 +1367,7 @@ export default class RestApi {
api.get('/:collection/:object', (req, res, next) => {
const { collection } = req
if (swaggerEndpoints.includes(collection.id)) {
if (swaggerEndpoints[collection.id] !== undefined) {
return next('route')
}
let result = req.object
@@ -1522,9 +1531,10 @@ export default class RestApi {
res.json({ ...action })
})
api.post('/:collection/:object/actions/:action', json(), (req, res, next) => {
const { collection } = req
const { action } = req.params
const fn = req.collection.actions?.[action]
if (fn === undefined) {
if (fn === undefined || swaggerEndpoints[collection.id]?.actions?.[action]) {
return next()
}