feat(@xen-orchestra/rest-api): expose POST srs/:id/vdis (#8984)

This commit is contained in:
Mathieu
2025-09-19 10:13:19 +02:00
committed by GitHub
parent c8827ab076
commit 067ee26715
6 changed files with 80 additions and 21 deletions

View File

@@ -84,6 +84,23 @@ export interface Xapi {
pool_emergencyShutdown(): Promise<void>
resumeVm(id: XoVm['id']): Promise<void>
unpauseVm(id: XoVm['id']): Promise<void>
SR_importVdi(
ref: XenApiSr['$ref'],
stream: Readable,
opts?: {
format?: SUPPORTED_VDI_FORMAT
name_description?: XoVdi['name_description']
name_label?: XoVdi['name_label']
other_config?: XoVdi['other_config']
read_only?: boolean
sharable?: boolean
SR?: XenApiSr['$ref']
tags?: XoVdi['tags']
type?: XoVdi['type']
virtual_size?: XoVdi['size']
xenstore_data?: Record<string, string>
}
): Promise<XenApiVdi['$ref']>
startVm(
id: XoVm['id'],
opts?: {

View File

@@ -3,6 +3,10 @@ export const vdiIds = [
'/rest/v0/vdis/771d5baf-4364-42f9-8c92-8e5fe08b332a',
]
export const vdiId = {
id: '5e13f673-760e-41be-826e-620d16b7f43b',
}
export const partialVdis = [
{
type: 'VDI',

View File

@@ -1,14 +1,16 @@
import { Example, Get, Path, Query, Request, Response, Route, Security, Tags } from 'tsoa'
import { Example, Get, Path, Post, Query, Request, Response, Route, Security, SuccessResponse, Tags } from 'tsoa'
import { inject } from 'inversify'
import { provide } from 'inversify-binding-decorators'
import { Request as ExRequest } from 'express'
import type { XoAlarm, XoSr } from '@vates/types'
import { SUPPORTED_VDI_FORMAT, XenApiVdi, XoVdi, type XoAlarm, type XoSr } from '@vates/types'
import { AlarmService } from '../alarms/alarm.service.mjs'
import { BASE_URL } from '../index.mjs'
import { escapeUnsafeComplexMatcher } from '../helpers/utils.helper.mjs'
import { genericAlarmsExample } from '../open-api/oa-examples/alarm.oa-example.mjs'
import { notFoundResp, unauthorizedResp, type Unbrand } from '../open-api/common/response.common.mjs'
import { createdResp, notFoundResp, unauthorizedResp, type Unbrand } from '../open-api/common/response.common.mjs'
import { partialSrs, sr, srIds } from '../open-api/oa-examples/sr.oa-example.mjs'
import { vdiId } from '../open-api/oa-examples/vdi.oa-example.mjs'
import { RestApi } from '../rest-api/rest-api.mjs'
import type { SendObjects } from '../helpers/helper.type.mjs'
import { XapiXoController } from '../abstract-classes/xapi-xo-controller.mjs'
@@ -79,4 +81,42 @@ export class SrController extends XapiXoController<XoSr> {
return this.sendObjects(Object.values(alarms), req, 'alarms')
}
/**
* Import an exported VDI
* @example id "c4284e12-37c9-7967-b9e8-83ef229c3e03"
* @example name_label "VDI_foo_import"
* @example name_description "VDI imported by the REST API"
* @example raw true
*/
@Example(vdiId)
@Post('{id}/vdis')
@Tags('vdis')
@SuccessResponse(createdResp.status, 'VDI imported')
@Response(notFoundResp.status, notFoundResp.description)
async srImportVdi(
@Request() req: ExRequest & { length?: number },
@Path() id: string,
@Query() name_label?: string,
@Query() name_description?: string,
@Query() raw?: boolean
): Promise<{ id: Unbrand<XoVdi>['id'] }> {
const xapiSr = this.getXapiObject(id as XoSr['id'])
const xapi = xapiSr.$xapi
if (req.headers['content-length'] !== undefined) {
req.length = +req.headers['content-length']
}
const vdiRef = await xapi.SR_importVdi(xapiSr.$ref, req, {
format: raw ? SUPPORTED_VDI_FORMAT.raw : SUPPORTED_VDI_FORMAT.vhd,
name_label,
name_description,
})
const vdiId = await xapi.getField<XenApiVdi, 'uuid'>('VDI', vdiRef, 'uuid')
this.setHeader('Location', `${BASE_URL}/vdis/${vdiId}`)
return { id: vdiId }
}
}

View File

@@ -31,6 +31,21 @@
}
}
},
"/srs/{id}/vdis": {
"post": {
"requestBody": {
"required": true,
"content": {
"application/octet-stream": {
"schema": {
"type": "string",
"format": "binary"
}
}
}
}
}
},
"/docs/swagger.json": {
"get": {
"operationId": "swaggerSpec",

View File

@@ -20,6 +20,7 @@
- `DELETE /rest/v0/vdis/<vdi-id>` (PR [#8961](https://github.com/vatesfr/xen-orchestra/pull/8961))
- `DELETE /rest/v0/vdi-snapshots/<vdi-snapshot-id>` (PR [#8961](https://github.com/vatesfr/xen-orchestra/pull/8961))
- `POST /rest/v0/tasks/<task-id>/actions/abort` (PR [#8908](https://github.com/vatesfr/xen-orchestra/pull/8908))
- `POST /rest/v0/srs/<sr-id>/vdis` (PR [#8984](https://github.com/vatesfr/xen-orchestra/pull/8984))
- `GET /rest/v0/vdis/<vdi-id>.(raw|vhd)` (PR [#8923](http://github.com/vatesfr/xen-orchestra/pull/8923))
- `GET /rest/v0/vdi-snapshots/<vdi-snapshot-id>.(raw|vhd)` (PR [#8923](http://github.com/vatesfr/xen-orchestra/pull/8923))
- `GET /rest/v0/vms/<vm-id>.(xva|ova)` (PR [#8929](https://github.com/vatesfr/xen-orchestra/pull/8929))

View File

@@ -12,7 +12,6 @@ import cloneDeep from 'lodash/cloneDeep.js'
import path from 'node:path'
import pick from 'lodash/pick.js'
import * as CM from 'complex-matcher'
import { VDI_FORMAT_RAW, VDI_FORMAT_VHD } from '@xen-orchestra/xapi'
import { getUserPublicProperties, isAlarm } from '../utils.mjs'
import { compileXoJsonSchema } from './_xoJsonSchema.mjs'
@@ -1016,23 +1015,6 @@ export default class RestApi {
})
)
api.post(
'/:collection(srs)/:object/vdis',
wrap(async (req, res) => {
const sr = req.xapiObject
req.length = ifDef(req.headers['content-length'], Number)
const { name_label, name_description, raw } = req.query
const vdiRef = await sr.$importVdi(req, {
format: raw !== undefined ? VDI_FORMAT_RAW : VDI_FORMAT_VHD,
name_label,
name_description,
})
res.end(await sr.$xapi.getField('VDI', vdiRef, 'uuid'))
})
)
setupRestApi(express, app)
}