@xen-orchestra/rest-api 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/abstract-classes/base-controller.mjs +37 -0
- package/dist/abstract-classes/xapi-xo-controller.mjs +5 -10
- package/dist/abstract-classes/xo-controller.mjs +40 -0
- package/dist/helpers/object-wrapper.helper.mjs +2 -2
- package/dist/hosts/host.controller.mjs +75 -0
- package/dist/index.mjs +4 -3
- package/dist/middlewares/generic-error-handler.middleware.mjs +5 -1
- package/dist/open-api/common/response.common.mjs +21 -0
- package/dist/open-api/oa-examples/host.oa-example.mjs +777 -0
- package/dist/open-api/oa-examples/server.oa-example.mjs +28 -0
- package/dist/open-api/oa-examples/sr.oa-example.mjs +81 -0
- package/dist/open-api/oa-examples/task.oa-example.mjs +1 -0
- package/dist/open-api/oa-examples/vbd.oa-example.mjs +34 -0
- package/dist/open-api/oa-examples/vdi-snapshot.oa-example.mjs +42 -0
- package/dist/open-api/oa-examples/vdi.oa-example.mjs +84 -0
- package/dist/open-api/oa-examples/vm.oa-example.mjs +144 -0
- package/dist/open-api/routes/routes.js +603 -15
- package/dist/rest-api/rest-api.mjs +9 -0
- package/dist/servers/server.controller.mjs +60 -0
- package/dist/srs/sr.controller.mjs +59 -0
- package/dist/vbds/vbd.controller.mjs +61 -0
- package/dist/vdi-snapshots/vdi-snapshot.controller.mjs +59 -0
- package/dist/vdis/vdi.controller.mjs +59 -0
- package/dist/vms/vm.controller.mjs +60 -4
- package/open-api/spec/swagger.json +11219 -737
- package/package.json +2 -2
- package/tsoa.json +3 -0
package/README.md
CHANGED
|
@@ -28,6 +28,7 @@ The REST API is based on the `TSOA` framework and therefore we use decorators a
|
|
|
28
28
|
@Routes('foo')
|
|
29
29
|
@Security('*')
|
|
30
30
|
@Response(401)
|
|
31
|
+
@Tags('foo')
|
|
31
32
|
@provide(Foo)
|
|
32
33
|
class Foo extends Controller {}
|
|
33
34
|
```
|
|
@@ -47,6 +48,7 @@ class Foo extends Controller {
|
|
|
47
48
|
@Example(['foo', 'bar'])
|
|
48
49
|
@Get('{id}')
|
|
49
50
|
@Security('*')
|
|
51
|
+
@SuccessResponse(202)
|
|
50
52
|
@Response(404)
|
|
51
53
|
getFoo(@Path() id: string) {
|
|
52
54
|
return this.getFoo(id)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Controller } from 'tsoa';
|
|
2
|
+
import { BASE_URL } from '../index.mjs';
|
|
3
|
+
import { makeObjectMapper } from '../helpers/object-wrapper.helper.mjs';
|
|
4
|
+
const noop = () => { };
|
|
5
|
+
export class BaseController extends Controller {
|
|
6
|
+
restApi;
|
|
7
|
+
constructor(restApi) {
|
|
8
|
+
super();
|
|
9
|
+
this.restApi = restApi;
|
|
10
|
+
}
|
|
11
|
+
sendObjects(objects, req) {
|
|
12
|
+
const mapper = makeObjectMapper(req);
|
|
13
|
+
const mappedObjects = objects.map(mapper);
|
|
14
|
+
return mappedObjects;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* statusCode must represent the status code in case of a synchronous request. Default 200
|
|
18
|
+
*/
|
|
19
|
+
async createAction(cb, { statusCode = 200, sync = false, taskProperties, }) {
|
|
20
|
+
taskProperties.name = 'REST API: ' + taskProperties.name;
|
|
21
|
+
taskProperties.type = 'xo:rest-api:action';
|
|
22
|
+
const task = this.restApi.tasks.create(taskProperties);
|
|
23
|
+
const pResult = task.run(cb);
|
|
24
|
+
if (sync) {
|
|
25
|
+
this.setStatus(statusCode);
|
|
26
|
+
return pResult;
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
pResult.catch(noop);
|
|
30
|
+
const location = `${BASE_URL}/tasks/${task.id}`;
|
|
31
|
+
this.setStatus(202);
|
|
32
|
+
this.setHeader('Location', location);
|
|
33
|
+
this.setHeader('Content-Type', 'text/plain');
|
|
34
|
+
return location;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import * as CM from 'complex-matcher';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
export class XapiXoController extends Controller {
|
|
2
|
+
import { BaseController } from './base-controller.mjs';
|
|
3
|
+
export class XapiXoController extends BaseController {
|
|
5
4
|
#type;
|
|
6
|
-
restApi;
|
|
7
5
|
constructor(type, restApi) {
|
|
8
|
-
super();
|
|
6
|
+
super(restApi);
|
|
9
7
|
this.#type = type;
|
|
10
|
-
this.restApi = restApi;
|
|
11
8
|
}
|
|
12
9
|
getObjects({ filter, limit } = {}) {
|
|
13
10
|
if (filter !== undefined) {
|
|
@@ -18,9 +15,7 @@ export class XapiXoController extends Controller {
|
|
|
18
15
|
getObject(id) {
|
|
19
16
|
return this.restApi.getObject(id, this.#type);
|
|
20
17
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const mappedObjects = objects.map(mapper);
|
|
24
|
-
return mappedObjects;
|
|
18
|
+
getXapiObject(maybeId) {
|
|
19
|
+
return this.restApi.getXapiObject(maybeId, this.#type);
|
|
25
20
|
}
|
|
26
21
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
8
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
9
|
+
};
|
|
10
|
+
import * as CM from 'complex-matcher';
|
|
11
|
+
import { inject } from 'inversify';
|
|
12
|
+
import { BaseController } from './base-controller.mjs';
|
|
13
|
+
import { RestApi } from '../rest-api/rest-api.mjs';
|
|
14
|
+
let XoController = class XoController extends BaseController {
|
|
15
|
+
constructor(restApi) {
|
|
16
|
+
super(restApi);
|
|
17
|
+
}
|
|
18
|
+
async getObjects({ filter, limit = Infinity } = {}) {
|
|
19
|
+
let objects = await this.getAllCollectionObjects();
|
|
20
|
+
if (filter !== undefined) {
|
|
21
|
+
const predicate = CM.parse(filter).createPredicate();
|
|
22
|
+
objects = objects.filter(predicate);
|
|
23
|
+
}
|
|
24
|
+
if (limit < objects.length) {
|
|
25
|
+
objects.length = limit;
|
|
26
|
+
}
|
|
27
|
+
const objectById = {};
|
|
28
|
+
objects.forEach(obj => {
|
|
29
|
+
objectById[obj.id] = obj;
|
|
30
|
+
});
|
|
31
|
+
return objectById;
|
|
32
|
+
}
|
|
33
|
+
async getObject(id) {
|
|
34
|
+
return this.getCollectionObject(id);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
XoController = __decorate([
|
|
38
|
+
__param(0, inject(RestApi))
|
|
39
|
+
], XoController);
|
|
40
|
+
export { XoController };
|
|
@@ -2,9 +2,9 @@ import path from 'node:path';
|
|
|
2
2
|
import pick from 'lodash/pick.js';
|
|
3
3
|
const { join } = path.posix;
|
|
4
4
|
export function makeObjectMapper(req, path = req.path) {
|
|
5
|
-
const makeUrl = ({ id }) => join(
|
|
5
|
+
const makeUrl = ({ id }) => join(path, typeof id === 'number' ? String(id) : id);
|
|
6
6
|
let objectMapper;
|
|
7
|
-
const { query
|
|
7
|
+
const { query } = req;
|
|
8
8
|
const { fields } = query;
|
|
9
9
|
if (fields === '*') {
|
|
10
10
|
objectMapper = object => ({
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
8
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
9
|
+
};
|
|
10
|
+
import { Example, Get, Path, Query, Request, Response, Route, Security, Tags } from 'tsoa';
|
|
11
|
+
import { inject } from 'inversify';
|
|
12
|
+
import { provide } from 'inversify-binding-decorators';
|
|
13
|
+
import { host, hostIds, hostStats, partialHosts } from '../open-api/oa-examples/host.oa-example.mjs';
|
|
14
|
+
import { RestApi } from '../rest-api/rest-api.mjs';
|
|
15
|
+
import { XapiXoController } from '../abstract-classes/xapi-xo-controller.mjs';
|
|
16
|
+
import { internalServerErrorResp, notFoundResp, unauthorizedResp, } from '../open-api/common/response.common.mjs';
|
|
17
|
+
let HostController = class HostController extends XapiXoController {
|
|
18
|
+
constructor(restApi) {
|
|
19
|
+
super('host', restApi);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* @example fields "id,name_label,productBrand"
|
|
23
|
+
* @example filter "productBrand:XCP-ng"
|
|
24
|
+
* @example limit 42
|
|
25
|
+
*/
|
|
26
|
+
getHosts(req, fields, filter, limit) {
|
|
27
|
+
return this.sendObjects(Object.values(this.getObjects({ filter, limit })), req);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* @example id "b61a5c92-700e-4966-a13b-00633f03eea8"
|
|
31
|
+
*/
|
|
32
|
+
getHost(id) {
|
|
33
|
+
return this.getObject(id);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Host must be running
|
|
37
|
+
* @example id "b61a5c92-700e-4966-a13b-00633f03eea8"
|
|
38
|
+
*/
|
|
39
|
+
getHostStats(id, granularity) {
|
|
40
|
+
return this.restApi.xoApp.getXapiHostStats(id, granularity);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
__decorate([
|
|
44
|
+
Example(hostIds),
|
|
45
|
+
Example(partialHosts),
|
|
46
|
+
Get(''),
|
|
47
|
+
__param(0, Request()),
|
|
48
|
+
__param(1, Query()),
|
|
49
|
+
__param(2, Query()),
|
|
50
|
+
__param(3, Query())
|
|
51
|
+
], HostController.prototype, "getHosts", null);
|
|
52
|
+
__decorate([
|
|
53
|
+
Example(host),
|
|
54
|
+
Get('{id}'),
|
|
55
|
+
Response(notFoundResp.status, notFoundResp.description),
|
|
56
|
+
__param(0, Path())
|
|
57
|
+
], HostController.prototype, "getHost", null);
|
|
58
|
+
__decorate([
|
|
59
|
+
Example(hostStats),
|
|
60
|
+
Get('{id}/stats'),
|
|
61
|
+
Response(notFoundResp.status, notFoundResp.description),
|
|
62
|
+
Response(422, 'Invalid granularity'),
|
|
63
|
+
Response(internalServerErrorResp.status, internalServerErrorResp.description),
|
|
64
|
+
__param(0, Path()),
|
|
65
|
+
__param(1, Query())
|
|
66
|
+
], HostController.prototype, "getHostStats", null);
|
|
67
|
+
HostController = __decorate([
|
|
68
|
+
Route('hosts'),
|
|
69
|
+
Security('*'),
|
|
70
|
+
Response(unauthorizedResp.status, unauthorizedResp.description),
|
|
71
|
+
Tags('hosts'),
|
|
72
|
+
provide(HostController),
|
|
73
|
+
__param(0, inject(RestApi))
|
|
74
|
+
], HostController);
|
|
75
|
+
export { HostController };
|
package/dist/index.mjs
CHANGED
|
@@ -8,13 +8,14 @@ import { setupContainer } from './ioc/ioc.mjs';
|
|
|
8
8
|
// https://github.com/nodejs/node/issues/51622
|
|
9
9
|
const require = createRequire(import.meta.url);
|
|
10
10
|
const swaggerOpenApiSpec = require('../open-api/spec/swagger.json');
|
|
11
|
+
export const BASE_URL = '/rest/v0';
|
|
11
12
|
export default function setupRestApi(express, xoApp) {
|
|
12
13
|
setupContainer(xoApp);
|
|
13
14
|
RegisterRoutes(express);
|
|
14
15
|
// do not register the doc at the root level, or it may lead to unwated behaviour
|
|
15
16
|
// uncomment when all endpoints are migrated to this API
|
|
16
17
|
// express.get('/rest/v0', (_req, res) => res.redirect('/rest/v0/docs'))
|
|
17
|
-
express.use(
|
|
18
|
-
express.use(
|
|
19
|
-
express.use(
|
|
18
|
+
express.use(`${BASE_URL}/docs`, swaggerUi.serve, swaggerUi.setup(swaggerOpenApiSpec));
|
|
19
|
+
express.use(BASE_URL, tsoaToXoErrorHandler);
|
|
20
|
+
express.use(BASE_URL, genericErrorHandler);
|
|
20
21
|
}
|
|
@@ -9,6 +9,7 @@ export default function genericErrorHandler(error, req, res, _next) {
|
|
|
9
9
|
res.status(500).json({ error });
|
|
10
10
|
return;
|
|
11
11
|
}
|
|
12
|
+
const responseError = { error: error.message };
|
|
12
13
|
if (noSuchObject.is(error)) {
|
|
13
14
|
res.status(404);
|
|
14
15
|
}
|
|
@@ -25,9 +26,12 @@ export default function genericErrorHandler(error, req, res, _next) {
|
|
|
25
26
|
res.status(501);
|
|
26
27
|
}
|
|
27
28
|
else {
|
|
29
|
+
if (error.name === 'XapiError') {
|
|
30
|
+
responseError.info = 'This is a XenServer/XCP-ng error, not an XO error';
|
|
31
|
+
}
|
|
28
32
|
res.status(500);
|
|
29
33
|
log.error(error);
|
|
30
34
|
}
|
|
31
35
|
log.info(`[${req.method}] ${req.path} (${res.statusCode})`);
|
|
32
|
-
res.json(
|
|
36
|
+
res.json(responseError);
|
|
33
37
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const actionAsyncroneResp = {
|
|
2
|
+
status: 202,
|
|
3
|
+
description: 'Action executed asynchronously',
|
|
4
|
+
produce: 'text/plain',
|
|
5
|
+
};
|
|
6
|
+
export const unauthorizedResp = {
|
|
7
|
+
status: 401,
|
|
8
|
+
description: 'Authentication required',
|
|
9
|
+
};
|
|
10
|
+
export const notFoundResp = {
|
|
11
|
+
status: 404,
|
|
12
|
+
description: 'Resource not found',
|
|
13
|
+
};
|
|
14
|
+
export const noContentResp = {
|
|
15
|
+
status: 204,
|
|
16
|
+
description: 'No content',
|
|
17
|
+
};
|
|
18
|
+
export const internalServerErrorResp = {
|
|
19
|
+
status: 500,
|
|
20
|
+
description: 'Internal server error, XenServer/XCP-ng error',
|
|
21
|
+
};
|