@xen-orchestra/rest-api 0.33.0 → 0.35.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 +32 -0
- package/dist/backup-jobs/backup-job.service.mjs +4 -0
- package/dist/backup-repositories/backup-repositories.controller.mjs +197 -6
- package/dist/backup-repositories/backup-repository.service.mjs +46 -0
- package/dist/hosts/host.controller.mjs +332 -2
- package/dist/hosts/host.service.mjs +76 -0
- package/dist/ioc/ioc.mjs +8 -0
- package/dist/middlewares/acl.middleware.mjs +10 -1
- package/dist/middlewares/deprecated.middleware.mjs +10 -0
- package/dist/middlewares/generic-error-handler.middleware.mjs +1 -0
- package/dist/open-api/oa-examples/backup-repository.oa-example.mjs +7 -0
- package/dist/open-api/routes/routes.js +488 -20
- package/dist/open-api/schema/build-openapi-schema.mjs +8 -1
- package/dist/pbds/pbd.controller.mjs +10 -0
- package/dist/pools/pool.controller.mjs +48 -0
- package/dist/router/external-router.mjs +4 -1
- package/dist/srs/sr.controller.mjs +15 -0
- package/dist/vbds/vbd.controller.mjs +20 -1
- package/dist/vdi-snapshots/vdi-snapshot.controller.mjs +27 -2
- package/dist/vdis/vdi.controller.mjs +26 -2
- package/dist/vifs/vif.controller.mjs +79 -6
- package/dist/vm-snapshots/vm-snapshot.controller.mjs +2 -1
- package/dist/vm-templates/vm-template.controller.mjs +2 -1
- package/dist/vms/vm.controller.mjs +21 -3
- package/dist/vms/vm.service.mjs +12 -11
- package/open-api/spec/swagger.json +1701 -76
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -22,6 +22,29 @@ npm install --save @xen-orchestra/rest-api
|
|
|
22
22
|
|
|
23
23
|
The REST API is based on the `TSOA` framework and therefore we use decorators a lot to define the behavior of a route or a group of routes. To keep things easily visible, it is best to always use the decorators in the same order.
|
|
24
24
|
|
|
25
|
+
At the request of the DevOps team, any REST API PR that updates the OpenAPI specification must include a reviewer from the DevOps team. (Non-blocking for merge.)
|
|
26
|
+
|
|
27
|
+
### Resource consistency
|
|
28
|
+
|
|
29
|
+
For a given resource (VMs, VIFs, users, etc.), property names must be consistent across all endpoints.
|
|
30
|
+
|
|
31
|
+
> Example: a PATCH request on a VM must use the same property names as the VM representation returned by the REST API.
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
GET /vms/:id
|
|
35
|
+
{
|
|
36
|
+
name_label: 'Foo',
|
|
37
|
+
name_description: 'Foo Bar',
|
|
38
|
+
...
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
PATCH /vms/:id
|
|
42
|
+
{
|
|
43
|
+
name_label: 'Bar' // OK ✅
|
|
44
|
+
nameDescription: 'Bar Foo' // Not OK ❌
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
25
48
|
### Class decorator
|
|
26
49
|
|
|
27
50
|
```ts
|
|
@@ -46,6 +69,7 @@ class Foo extends Controller {
|
|
|
46
69
|
* @example id 1234
|
|
47
70
|
*/
|
|
48
71
|
@Example(['foo', 'bar'])
|
|
72
|
+
@Extension('x-mcp-exposure', 'allow')
|
|
49
73
|
@Get('{id}')
|
|
50
74
|
@Security('*')
|
|
51
75
|
@Middlewares(json())
|
|
@@ -162,6 +186,14 @@ getUser(@Path() id: string) { ... }
|
|
|
162
186
|
|
|
163
187
|
If you need to use a privilege that doesn't exist yet (e.g., `resource: 'vm', action: 'foo'`), you must register it in ACL Definition: here `@xen-orchestra/acl/src/actions/vm.mts`, add: `foo: true`.
|
|
164
188
|
|
|
189
|
+
### MCP exposure
|
|
190
|
+
|
|
191
|
+
All REST API endpoints must define an MCP exposure policy using the `@Extension` decorator.
|
|
192
|
+
|
|
193
|
+
- `@Extension('x-mcp-exposure', 'allow')` for all `GET` endpoints.
|
|
194
|
+
- `@Extension('x-mcp-exposure', 'confirm')` for all non-`GET` endpoints (`POST`, `PATCH`, `PUT`, `DELETE`, etc.).
|
|
195
|
+
- `@Extension('x-mcp-exposure', 'deny')` only for exceptional cases.
|
|
196
|
+
|
|
165
197
|
## Contributions
|
|
166
198
|
|
|
167
199
|
Contributions are _very_ welcomed, either on the documentation or on
|
|
@@ -45,6 +45,10 @@ export class BackupJobService {
|
|
|
45
45
|
if (schedule.enabled) {
|
|
46
46
|
return true;
|
|
47
47
|
}
|
|
48
|
+
const scheduleSequence = await this.#restApi.xoApp.findEnabledScheduleSequenceFromSchedule(schedule.id);
|
|
49
|
+
if (scheduleSequence !== undefined) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
48
52
|
}
|
|
49
53
|
catch (error) {
|
|
50
54
|
if (!noSuchObject.is(error, { id: maybeScheduleId, type: 'schedule' })) {
|
|
@@ -7,17 +7,24 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
7
|
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
8
8
|
return function (target, key) { decorator(target, key, paramIndex); }
|
|
9
9
|
};
|
|
10
|
-
import { Example, Extension, Get, Middlewares, Path, Query, Request, Response, Route, Security, Tags } from 'tsoa';
|
|
10
|
+
import { Body, Example, Extension, Get, Middlewares, Patch, Path, Post, Query, Request, Response, Route, Security, SuccessResponse, Tags, } from 'tsoa';
|
|
11
11
|
import { inject } from 'inversify';
|
|
12
12
|
import { provide } from 'inversify-binding-decorators';
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
13
|
+
import { json } from 'express';
|
|
14
|
+
import { forbiddenOperation } from 'xo-common/api-errors.js';
|
|
15
|
+
import { acl, actionsFromBody } from '../middlewares/acl.middleware.mjs';
|
|
16
|
+
import { asynchronousActionResp, badRequestResp, createdResp, forbiddenOperationResp, invalidParameters, noContentResp, internalServerErrorResp, notFoundResp, unauthorizedResp, } from '../open-api/common/response.common.mjs';
|
|
17
|
+
import { backupRepositoryIds, partialBackupRepositories, backupRepository, backupRepositoryId, backupRepositoryBenchmark, backupRepositoryHeath, } from '../open-api/oa-examples/backup-repository.oa-example.mjs';
|
|
16
18
|
import { XoController } from '../abstract-classes/xo-controller.mjs';
|
|
17
19
|
import { RestApi } from '../rest-api/rest-api.mjs';
|
|
20
|
+
import { BackupRepositoryService } from './backup-repository.service.mjs';
|
|
21
|
+
import { taskLocation } from '../open-api/oa-examples/task.oa-example.mjs';
|
|
22
|
+
import { ApiError } from '../helpers/error.helper.mjs';
|
|
18
23
|
let BackupRepositoryController = class BackupRepositoryController extends XoController {
|
|
19
|
-
|
|
24
|
+
#backupRepositoryService;
|
|
25
|
+
constructor(restApi, backupRepositoryService) {
|
|
20
26
|
super('backup-repository', restApi);
|
|
27
|
+
this.#backupRepositoryService = backupRepositoryService;
|
|
21
28
|
}
|
|
22
29
|
// --- abstract methods
|
|
23
30
|
getAllCollectionObjects() {
|
|
@@ -40,6 +47,16 @@ let BackupRepositoryController = class BackupRepositoryController extends XoCont
|
|
|
40
47
|
privilege: { action: 'read', resource: 'backup-repository' },
|
|
41
48
|
});
|
|
42
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Required privilege:
|
|
52
|
+
* - resource: backup-repository, action: create
|
|
53
|
+
*
|
|
54
|
+
* @example body { "name": "NFS Remote", "options": "vers=4", "proxy": "722d17b9-699b-59d2-8193-be1ac573d3de", "url": "nfs://192.168.100.225:/media/nfs" }
|
|
55
|
+
*/
|
|
56
|
+
async createBackupRepository(body) {
|
|
57
|
+
const backupRepository = await this.restApi.xoApp.createRemote(body);
|
|
58
|
+
return { id: backupRepository.id };
|
|
59
|
+
}
|
|
43
60
|
/**
|
|
44
61
|
* Required privilege:
|
|
45
62
|
* - resource: backup-repository, action: read
|
|
@@ -49,6 +66,93 @@ let BackupRepositoryController = class BackupRepositoryController extends XoCont
|
|
|
49
66
|
getRepository(id) {
|
|
50
67
|
return this.getObject(id);
|
|
51
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Forgets a backup repository configuration.
|
|
71
|
+
*
|
|
72
|
+
* A backup repository cannot be forgotten if it is referenced by any backup job (enabled or disabled).
|
|
73
|
+
*
|
|
74
|
+
* Required privilege:
|
|
75
|
+
* - resource: backup-repository, action: forget
|
|
76
|
+
*
|
|
77
|
+
* @example id "c4284e12-37c9-7967-b9e8-83ef229c3e03"
|
|
78
|
+
*/
|
|
79
|
+
async forgetBackupRepository(id, sync) {
|
|
80
|
+
const repositoryId = id;
|
|
81
|
+
const action = async () => {
|
|
82
|
+
const referencingJobs = await this.#backupRepositoryService.getReferencingJobs(repositoryId);
|
|
83
|
+
if (referencingJobs.length > 0) {
|
|
84
|
+
throw forbiddenOperation('forget backup repository', `repository is referenced by ${referencingJobs.length} backup job(s): ${referencingJobs.join(', ')}`);
|
|
85
|
+
}
|
|
86
|
+
await this.restApi.xoApp.removeRemote(repositoryId);
|
|
87
|
+
};
|
|
88
|
+
return this.createAction(action, {
|
|
89
|
+
sync,
|
|
90
|
+
statusCode: noContentResp.status,
|
|
91
|
+
taskProperties: {
|
|
92
|
+
name: 'forget backup repository',
|
|
93
|
+
objectId: repositoryId,
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
/** Required privileges:
|
|
98
|
+
* - resource: backup-repository, action: update (grants all fields)
|
|
99
|
+
* - resource: backup-repository, action: update:enabled (if enabled is passed)
|
|
100
|
+
* - resource: backup-repository, action: update:name (if name is passed)
|
|
101
|
+
* - resource: backup-repository, action: update:options (if options is passed)
|
|
102
|
+
* - resource: backup-repository, action: update:proxy (if proxy is passed)
|
|
103
|
+
* - resource: backup-repository, action: update:url (if url is passed)
|
|
104
|
+
*
|
|
105
|
+
* @example id "c4284e12-37c9-7967-b9e8-83ef229c3e03"
|
|
106
|
+
* @example body { "enabled": true, "name": "NFS Remote", "options": "vers=4", "proxy": "722d17b9-699b-59d2-8193-be1ac573d3de", "url": "nfs://192.168.100.225:/media/nfs" }
|
|
107
|
+
*/
|
|
108
|
+
async updateBackupRepository(id, body) {
|
|
109
|
+
await this.restApi.xoApp.updateRemote(id, body);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Pings the backup-repository to check its health
|
|
113
|
+
*
|
|
114
|
+
* Required privilege:
|
|
115
|
+
* - resource: backup-repository, action: read
|
|
116
|
+
*
|
|
117
|
+
* @example id "c4284e12-37c9-7967-b9e8-83ef229c3e03"
|
|
118
|
+
*/
|
|
119
|
+
async getBackupRepositoryHealth(id) {
|
|
120
|
+
return this.restApi.xoApp.pingRemote(id);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Runs a benchmark for write and read speed on the Backup-repository.
|
|
124
|
+
* Saves the benchmark result on the BR and returns the results, speeds are in bytes/sec.
|
|
125
|
+
* 502 if the BR cannot be reached, 400 if there was a problem during the benchmark with the error.
|
|
126
|
+
*
|
|
127
|
+
* Required privilege:
|
|
128
|
+
* - resource: backup-repository, action: benchmark
|
|
129
|
+
*
|
|
130
|
+
* @example id "c4284e12-37c9-7967-b9e8-83ef229c3e03"
|
|
131
|
+
*/
|
|
132
|
+
benchmarkBackupRepository(id, sync) {
|
|
133
|
+
const backupRepositoryId = id;
|
|
134
|
+
const action = async () => {
|
|
135
|
+
let result;
|
|
136
|
+
try {
|
|
137
|
+
result = await this.restApi.xoApp.testRemote(backupRepositoryId);
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
throw new ApiError('Backup repository unreachable', 502);
|
|
141
|
+
}
|
|
142
|
+
if (!result.success) {
|
|
143
|
+
throw new ApiError('Benchmark failed', 400, { data: result });
|
|
144
|
+
}
|
|
145
|
+
return result;
|
|
146
|
+
};
|
|
147
|
+
return this.createAction(action, {
|
|
148
|
+
sync,
|
|
149
|
+
statusCode: 200,
|
|
150
|
+
taskProperties: {
|
|
151
|
+
name: 'benchmark backup repository',
|
|
152
|
+
objectId: backupRepositoryId,
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
}
|
|
52
156
|
};
|
|
53
157
|
__decorate([
|
|
54
158
|
Example(backupRepositoryIds),
|
|
@@ -63,6 +167,22 @@ __decorate([
|
|
|
63
167
|
__param(4, Query()),
|
|
64
168
|
__param(5, Query())
|
|
65
169
|
], BackupRepositoryController.prototype, "getRepositories", null);
|
|
170
|
+
__decorate([
|
|
171
|
+
Example(backupRepositoryId),
|
|
172
|
+
Post(''),
|
|
173
|
+
Middlewares([
|
|
174
|
+
json(),
|
|
175
|
+
acl({
|
|
176
|
+
resource: 'backup-repository',
|
|
177
|
+
action: 'create',
|
|
178
|
+
object: ({ req }) => req.body,
|
|
179
|
+
}),
|
|
180
|
+
]),
|
|
181
|
+
SuccessResponse(createdResp.status, createdResp.description),
|
|
182
|
+
Response(forbiddenOperationResp.status, forbiddenOperationResp.description),
|
|
183
|
+
Response(invalidParameters.status, invalidParameters.description),
|
|
184
|
+
__param(0, Body())
|
|
185
|
+
], BackupRepositoryController.prototype, "createBackupRepository", null);
|
|
66
186
|
__decorate([
|
|
67
187
|
Example(backupRepository),
|
|
68
188
|
Extension('x-mcp-exposure', 'allow'),
|
|
@@ -77,6 +197,76 @@ __decorate([
|
|
|
77
197
|
Response(notFoundResp.status, notFoundResp.description),
|
|
78
198
|
__param(0, Path())
|
|
79
199
|
], BackupRepositoryController.prototype, "getRepository", null);
|
|
200
|
+
__decorate([
|
|
201
|
+
Example(taskLocation),
|
|
202
|
+
Extension('x-mcp-exposure', 'confirm'),
|
|
203
|
+
Post('{id}/actions/forget'),
|
|
204
|
+
Middlewares(acl({
|
|
205
|
+
resource: 'backup-repository',
|
|
206
|
+
action: 'forget',
|
|
207
|
+
objectId: 'params.id',
|
|
208
|
+
getObject: ({ restApi }) => restApi.xoApp.getRemote,
|
|
209
|
+
})),
|
|
210
|
+
SuccessResponse(noContentResp.status, noContentResp.description),
|
|
211
|
+
Response(forbiddenOperationResp.status, forbiddenOperationResp.description),
|
|
212
|
+
Response(notFoundResp.status, notFoundResp.description),
|
|
213
|
+
__param(0, Path()),
|
|
214
|
+
__param(1, Query())
|
|
215
|
+
], BackupRepositoryController.prototype, "forgetBackupRepository", null);
|
|
216
|
+
__decorate([
|
|
217
|
+
Patch('{id}'),
|
|
218
|
+
Middlewares([
|
|
219
|
+
json(),
|
|
220
|
+
acl({
|
|
221
|
+
resource: 'backup-repository',
|
|
222
|
+
actions: actionsFromBody(['update:enabled', 'update:name', 'update:options', 'update:proxy', 'update:url']),
|
|
223
|
+
objectId: 'params.id',
|
|
224
|
+
getObject: ({ restApi }) => restApi.xoApp.getRemote,
|
|
225
|
+
}),
|
|
226
|
+
]),
|
|
227
|
+
SuccessResponse(noContentResp.status, noContentResp.description),
|
|
228
|
+
Response(forbiddenOperationResp.status, forbiddenOperationResp.description),
|
|
229
|
+
Response(notFoundResp.status, notFoundResp.description),
|
|
230
|
+
Response(invalidParameters.status, invalidParameters.description),
|
|
231
|
+
__param(0, Path()),
|
|
232
|
+
__param(1, Body())
|
|
233
|
+
], BackupRepositoryController.prototype, "updateBackupRepository", null);
|
|
234
|
+
__decorate([
|
|
235
|
+
Example(backupRepositoryHeath),
|
|
236
|
+
Extension('x-mcp-exposure', 'allow'),
|
|
237
|
+
Get('{id}/health'),
|
|
238
|
+
Middlewares(acl({
|
|
239
|
+
resource: 'backup-repository',
|
|
240
|
+
action: 'read',
|
|
241
|
+
objectId: 'params.id',
|
|
242
|
+
getObject: ({ restApi }) => restApi.xoApp.getRemote,
|
|
243
|
+
})),
|
|
244
|
+
SuccessResponse(200, 'OK'),
|
|
245
|
+
Response(forbiddenOperationResp.status, forbiddenOperationResp.description),
|
|
246
|
+
Response(internalServerErrorResp.status, internalServerErrorResp.description),
|
|
247
|
+
__param(0, Path())
|
|
248
|
+
], BackupRepositoryController.prototype, "getBackupRepositoryHealth", null);
|
|
249
|
+
__decorate([
|
|
250
|
+
Example(taskLocation),
|
|
251
|
+
Example(backupRepositoryBenchmark),
|
|
252
|
+
Extension('x-mcp-exposure', 'confirm'),
|
|
253
|
+
Post('{id}/actions/benchmark'),
|
|
254
|
+
Middlewares(acl({
|
|
255
|
+
resource: 'backup-repository',
|
|
256
|
+
action: 'benchmark',
|
|
257
|
+
objectId: 'params.id',
|
|
258
|
+
getObject: ({ restApi }) => restApi.xoApp.getRemote,
|
|
259
|
+
})),
|
|
260
|
+
SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description),
|
|
261
|
+
Response(200, 'Ok'),
|
|
262
|
+
Response(400, 'Benchmark failed'),
|
|
263
|
+
Response(notFoundResp.status, notFoundResp.description),
|
|
264
|
+
Response(forbiddenOperationResp.status, forbiddenOperationResp.description),
|
|
265
|
+
Response(internalServerErrorResp.status, internalServerErrorResp.description),
|
|
266
|
+
Response(502, 'Backup repository unreachable'),
|
|
267
|
+
__param(0, Path()),
|
|
268
|
+
__param(1, Query())
|
|
269
|
+
], BackupRepositoryController.prototype, "benchmarkBackupRepository", null);
|
|
80
270
|
BackupRepositoryController = __decorate([
|
|
81
271
|
Route('backup-repositories'),
|
|
82
272
|
Security('*'),
|
|
@@ -84,6 +274,7 @@ BackupRepositoryController = __decorate([
|
|
|
84
274
|
Response(unauthorizedResp.status, unauthorizedResp.description),
|
|
85
275
|
Tags('backup-repositories'),
|
|
86
276
|
provide(BackupRepositoryController),
|
|
87
|
-
__param(0, inject(RestApi))
|
|
277
|
+
__param(0, inject(RestApi)),
|
|
278
|
+
__param(1, inject(BackupRepositoryService))
|
|
88
279
|
], BackupRepositoryController);
|
|
89
280
|
export { BackupRepositoryController };
|
|
@@ -0,0 +1,46 @@
|
|
|
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 { RestApi } from '../rest-api/rest-api.mjs';
|
|
11
|
+
import { inject } from 'inversify';
|
|
12
|
+
let BackupRepositoryService = class BackupRepositoryService {
|
|
13
|
+
#restApi;
|
|
14
|
+
constructor(restApi) {
|
|
15
|
+
this.#restApi = restApi;
|
|
16
|
+
}
|
|
17
|
+
isBackupRepositoryReferenced(idsToCheck, repositoryId) {
|
|
18
|
+
if (idsToCheck === undefined) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
const { id } = idsToCheck;
|
|
22
|
+
const ids = typeof id === 'string' ? [id] : id.__or;
|
|
23
|
+
return ids.includes(repositoryId);
|
|
24
|
+
}
|
|
25
|
+
async getReferencingJobs(repositoryId) {
|
|
26
|
+
const allJobs = await this.#restApi.xoApp.getAllJobs();
|
|
27
|
+
const referencingJobs = [];
|
|
28
|
+
for (const job of allJobs) {
|
|
29
|
+
if (job.type === 'backup' || job.type === 'metadataBackup') {
|
|
30
|
+
if (this.isBackupRepositoryReferenced(job.remotes, repositoryId)) {
|
|
31
|
+
referencingJobs.push(job.id);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
else if (job.type === 'mirrorBackup') {
|
|
35
|
+
if (job.sourceRemote === repositoryId || this.isBackupRepositoryReferenced(job.remotes, repositoryId)) {
|
|
36
|
+
referencingJobs.push(job.id);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return referencingJobs;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
BackupRepositoryService = __decorate([
|
|
44
|
+
__param(0, inject(RestApi))
|
|
45
|
+
], BackupRepositoryService);
|
|
46
|
+
export { BackupRepositoryService };
|