From 067ee267155d3de3bc093f8fe6ca44585b5cad9e Mon Sep 17 00:00:00 2001 From: Mathieu <70369997+MathieuRA@users.noreply.github.com> Date: Fri, 19 Sep 2025 10:13:19 +0200 Subject: [PATCH] feat(@xen-orchestra/rest-api): expose POST srs/:id/vdis (#8984) --- @vates/types/src/lib/xen-orchestra-xapi.mts | 17 +++++++ .../open-api/oa-examples/vdi.oa-example.mts | 4 ++ .../rest-api/src/srs/sr.controller.mts | 46 +++++++++++++++++-- @xen-orchestra/rest-api/tsoa.json | 15 ++++++ CHANGELOG.unreleased.md | 1 + packages/xo-server/src/xo-mixins/rest-api.mjs | 18 -------- 6 files changed, 80 insertions(+), 21 deletions(-) diff --git a/@vates/types/src/lib/xen-orchestra-xapi.mts b/@vates/types/src/lib/xen-orchestra-xapi.mts index 2030ebd478..ca9716b6b2 100644 --- a/@vates/types/src/lib/xen-orchestra-xapi.mts +++ b/@vates/types/src/lib/xen-orchestra-xapi.mts @@ -84,6 +84,23 @@ export interface Xapi { pool_emergencyShutdown(): Promise resumeVm(id: XoVm['id']): Promise unpauseVm(id: XoVm['id']): Promise + 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 + } + ): Promise startVm( id: XoVm['id'], opts?: { diff --git a/@xen-orchestra/rest-api/src/open-api/oa-examples/vdi.oa-example.mts b/@xen-orchestra/rest-api/src/open-api/oa-examples/vdi.oa-example.mts index 8d5e259291..b87299b1b7 100644 --- a/@xen-orchestra/rest-api/src/open-api/oa-examples/vdi.oa-example.mts +++ b/@xen-orchestra/rest-api/src/open-api/oa-examples/vdi.oa-example.mts @@ -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', diff --git a/@xen-orchestra/rest-api/src/srs/sr.controller.mts b/@xen-orchestra/rest-api/src/srs/sr.controller.mts index 61f7383197..1ffd417222 100644 --- a/@xen-orchestra/rest-api/src/srs/sr.controller.mts +++ b/@xen-orchestra/rest-api/src/srs/sr.controller.mts @@ -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 { 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['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('VDI', vdiRef, 'uuid') + + this.setHeader('Location', `${BASE_URL}/vdis/${vdiId}`) + + return { id: vdiId } + } } diff --git a/@xen-orchestra/rest-api/tsoa.json b/@xen-orchestra/rest-api/tsoa.json index 890dbe8daa..33d87e33f2 100644 --- a/@xen-orchestra/rest-api/tsoa.json +++ b/@xen-orchestra/rest-api/tsoa.json @@ -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", diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index 7e4f024b93..6a3e580db2 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -20,6 +20,7 @@ - `DELETE /rest/v0/vdis/` (PR [#8961](https://github.com/vatesfr/xen-orchestra/pull/8961)) - `DELETE /rest/v0/vdi-snapshots/` (PR [#8961](https://github.com/vatesfr/xen-orchestra/pull/8961)) - `POST /rest/v0/tasks//actions/abort` (PR [#8908](https://github.com/vatesfr/xen-orchestra/pull/8908)) + - `POST /rest/v0/srs//vdis` (PR [#8984](https://github.com/vatesfr/xen-orchestra/pull/8984)) - `GET /rest/v0/vdis/.(raw|vhd)` (PR [#8923](http://github.com/vatesfr/xen-orchestra/pull/8923)) - `GET /rest/v0/vdi-snapshots/.(raw|vhd)` (PR [#8923](http://github.com/vatesfr/xen-orchestra/pull/8923)) - `GET /rest/v0/vms/.(xva|ova)` (PR [#8929](https://github.com/vatesfr/xen-orchestra/pull/8929)) diff --git a/packages/xo-server/src/xo-mixins/rest-api.mjs b/packages/xo-server/src/xo-mixins/rest-api.mjs index d9c3e56e95..7315bfb418 100644 --- a/packages/xo-server/src/xo-mixins/rest-api.mjs +++ b/packages/xo-server/src/xo-mixins/rest-api.mjs @@ -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) }