@xen-orchestra/rest-api 0.30.1 → 0.31.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/dist/helpers/stream.helper.mjs +17 -2
- package/dist/hosts/host.controller.mjs +29 -2
- package/dist/index.mjs +6 -0
- package/dist/ioc/ioc.mjs +16 -0
- package/dist/licenses/license.service.mjs +23 -0
- package/dist/open-api/routes/routes.js +71 -15
- package/dist/open-api/schema/build-openapi-schema.mjs +29 -0
- package/dist/router/external-router.mjs +260 -0
- package/dist/router/types.mjs +7 -0
- package/dist/srs/sr.controller.mjs +24 -3
- package/dist/srs/sr.service.mjs +27 -0
- package/dist/vifs/vif.controller.mjs +8 -1
- package/dist/vms/vm.controller.mjs +59 -0
- package/open-api/spec/swagger.json +229 -25
- package/package.json +9 -6
- package/turbo.json +8 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Build OpenApi schema from our FieldDefinition
|
|
2
|
+
export function buildOpenApiSchema(def) {
|
|
3
|
+
const schema = {
|
|
4
|
+
type: 'object',
|
|
5
|
+
properties: {},
|
|
6
|
+
};
|
|
7
|
+
const required = [];
|
|
8
|
+
for (const [key, field] of Object.entries(def)) {
|
|
9
|
+
const property = {};
|
|
10
|
+
if (field.type === 'enum') {
|
|
11
|
+
property.type = 'string';
|
|
12
|
+
property.enum = field.enum;
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
property.type = field.type;
|
|
16
|
+
}
|
|
17
|
+
if (field.example !== undefined) {
|
|
18
|
+
property.example = field.example;
|
|
19
|
+
}
|
|
20
|
+
schema.properties[key] = property;
|
|
21
|
+
if (!field.optional) {
|
|
22
|
+
required.push(key);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (required.length) {
|
|
26
|
+
schema.required = required;
|
|
27
|
+
}
|
|
28
|
+
return schema;
|
|
29
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { pipeline } from 'node:stream/promises';
|
|
2
|
+
import { Readable } from 'node:stream';
|
|
3
|
+
import { Router, json, urlencoded, text, raw } from 'express';
|
|
4
|
+
import { invalidParameters } from 'xo-common/api-errors.js';
|
|
5
|
+
import { createLogger } from '@xen-orchestra/log';
|
|
6
|
+
import { z, ZodError } from 'zod';
|
|
7
|
+
import { createSchema } from 'zod-openapi';
|
|
8
|
+
import { buildOpenApiSchema } from '../open-api/schema/build-openapi-schema.mjs';
|
|
9
|
+
import { makeJsonStream, makeNdJsonStream } from '../helpers/stream.helper.mjs';
|
|
10
|
+
import { expressAuthentication } from '../middlewares/authentication.middleware.mjs';
|
|
11
|
+
import { acl } from '../middlewares/acl.middleware.mjs';
|
|
12
|
+
import { iocContainer } from '../ioc/ioc.mjs';
|
|
13
|
+
import { RestApi } from '../rest-api/rest-api.mjs';
|
|
14
|
+
import { CONTENT_TYPE_BY_MIDDLEWARE_NAME, } from './types.mjs';
|
|
15
|
+
const log = createLogger('xo:rest-api:external-router');
|
|
16
|
+
// Returns a mountExternalRoute function that allows routes to be added dynamically to the express router and a reference to the router
|
|
17
|
+
export function createExternalRouter(swaggerOpenApiSpec) {
|
|
18
|
+
const externalRouter = Router();
|
|
19
|
+
const mountExternalRoute = (route) => {
|
|
20
|
+
const restApi = iocContainer.get(RestApi);
|
|
21
|
+
const tags = route.tags ?? [];
|
|
22
|
+
// Build zod schema for input validation
|
|
23
|
+
const paramsSchema = route.params && buildZodSchema(route.params);
|
|
24
|
+
const querySchema = route.query && buildZodSchema(route.query);
|
|
25
|
+
const bodySchema = route.body && buildZodSchema(route.body);
|
|
26
|
+
// Format route params for express
|
|
27
|
+
const expressEndpoint = route.endpoint.replace(/{(\w+)}/g, ':$1');
|
|
28
|
+
// Resolve middleware descriptors to actual Express RequestHandlers
|
|
29
|
+
const middlewareDescriptors = route.middlewares ?? [];
|
|
30
|
+
const middlewares = middlewareDescriptors.map(resolveMiddleware);
|
|
31
|
+
// Set the body content type based on the used middleware
|
|
32
|
+
const bodyContentType = middlewareDescriptors
|
|
33
|
+
.map(descriptor => CONTENT_TYPE_BY_MIDDLEWARE_NAME[descriptor.name])
|
|
34
|
+
.find(Boolean);
|
|
35
|
+
// Add route to router
|
|
36
|
+
externalRouter[route.method](expressEndpoint, ...middlewares, async (req, res, next) => {
|
|
37
|
+
try {
|
|
38
|
+
// Handle authentication if required
|
|
39
|
+
if (route.security === undefined)
|
|
40
|
+
route.security = '*';
|
|
41
|
+
await expressAuthentication(req, route.security, route.scope === 'acl' ? ['acl'] : []);
|
|
42
|
+
// Coerces query boolean from string to boolean
|
|
43
|
+
coerceBooleanQueryParams(req.query, route.query);
|
|
44
|
+
// Validate inputs, throws if invalid
|
|
45
|
+
paramsSchema?.parse(req.params);
|
|
46
|
+
querySchema?.parse(req.query);
|
|
47
|
+
bodySchema?.parse(req.body);
|
|
48
|
+
// Per-request createAction helper pre-wired with res and restApi.tasks
|
|
49
|
+
const createAction = async (cb, { sync = false, statusCode = 200, taskProperties, }) => {
|
|
50
|
+
taskProperties.name = 'REST API: ' + taskProperties.name;
|
|
51
|
+
taskProperties.type = 'xo:rest-api:action';
|
|
52
|
+
const task = restApi.tasks.create(taskProperties);
|
|
53
|
+
const pResult = task.run(() => cb(task));
|
|
54
|
+
if (sync) {
|
|
55
|
+
const result = await pResult;
|
|
56
|
+
res.status(statusCode);
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
pResult.catch(() => { });
|
|
61
|
+
res.status(202).set('Location', `/rest/v0/tasks/${task.id}`).json({ taskId: task.id });
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
// Call the route callback with the right context and parameters
|
|
66
|
+
const result = await route.callback({ req, res, next, restApi, createAction });
|
|
67
|
+
// Handle result formatting if the callback didn't already send a response
|
|
68
|
+
if (!res.headersSent) {
|
|
69
|
+
if (result === undefined) {
|
|
70
|
+
res.status(204).end();
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
const isIterable = result != null &&
|
|
74
|
+
typeof result !== 'string' &&
|
|
75
|
+
!Buffer.isBuffer(result) &&
|
|
76
|
+
typeof (result[Symbol.iterator] ?? result[Symbol.asyncIterator]) === 'function';
|
|
77
|
+
if (isIterable) {
|
|
78
|
+
await sendObjects(result, req, res);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
res.json(result);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
// Handle zod validation error to run with existing error middleware
|
|
88
|
+
if (error instanceof ZodError) {
|
|
89
|
+
try {
|
|
90
|
+
throw invalidParameters(undefined, error.issues);
|
|
91
|
+
}
|
|
92
|
+
catch (xoError) {
|
|
93
|
+
return next(xoError);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return next(error);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
// Add route to the swagger documentation in memory
|
|
100
|
+
addPathToSwagger(route.method, route.endpoint, tags, paramsSchema, querySchema, bodySchema, bodyContentType, route.responses, swaggerOpenApiSpec, route.description);
|
|
101
|
+
// Return an unregister function to remove the route when they are not needed anymore
|
|
102
|
+
return () => {
|
|
103
|
+
const index = externalRouter.stack.findIndex(layer => {
|
|
104
|
+
// layer.route.methods is not exposed so we need to redefine the type
|
|
105
|
+
const layerRoutes = layer.route;
|
|
106
|
+
return layerRoutes?.path === expressEndpoint && layerRoutes.methods[route.method] === true;
|
|
107
|
+
});
|
|
108
|
+
if (index !== -1) {
|
|
109
|
+
externalRouter.stack.splice(index, 1);
|
|
110
|
+
removePathFromSwagger(route.endpoint, route.method, swaggerOpenApiSpec);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
log.warn(`Failed to unregister external route ${route.method.toUpperCase()} ${route.endpoint} from REST API.`);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
};
|
|
117
|
+
return { mountExternalRoute, externalRouter };
|
|
118
|
+
}
|
|
119
|
+
// Add the rest route path to the rest api swagger with the input schema and response definitions
|
|
120
|
+
function addPathToSwagger(method, fullPath, tags, paramsSchema, querySchema, bodySchema, bodyContentType, responseDefinitions = [], swaggerOpenApiSpec, description) {
|
|
121
|
+
if (!swaggerOpenApiSpec.paths[fullPath]) {
|
|
122
|
+
swaggerOpenApiSpec.paths[fullPath] = {};
|
|
123
|
+
}
|
|
124
|
+
const operation = {
|
|
125
|
+
tags: [...tags, 'external'],
|
|
126
|
+
description,
|
|
127
|
+
parameters: [],
|
|
128
|
+
responses: {},
|
|
129
|
+
};
|
|
130
|
+
if (paramsSchema)
|
|
131
|
+
operation.parameters?.push(...extractParametersFromZod(paramsSchema, 'path'));
|
|
132
|
+
if (querySchema)
|
|
133
|
+
operation.parameters?.push(...extractParametersFromZod(querySchema, 'query'));
|
|
134
|
+
if (bodySchema) {
|
|
135
|
+
const { schema: bodyJson } = createSchema(bodySchema, { io: 'input' });
|
|
136
|
+
operation.requestBody = {
|
|
137
|
+
required: true,
|
|
138
|
+
content: {
|
|
139
|
+
[bodyContentType ?? 'text/plain']: { schema: bodyJson },
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
if (responseDefinitions?.length) {
|
|
144
|
+
for (const responseDefinition of responseDefinitions) {
|
|
145
|
+
const response = {
|
|
146
|
+
description: responseDefinition.description,
|
|
147
|
+
};
|
|
148
|
+
if (responseDefinition.schema) {
|
|
149
|
+
response.content = {
|
|
150
|
+
'application/json': {
|
|
151
|
+
schema: buildOpenApiSchema(responseDefinition.schema),
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
operation.responses[responseDefinition.status] = response;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
// default 200 response
|
|
160
|
+
operation.responses['200'] = { description: 'Success' };
|
|
161
|
+
}
|
|
162
|
+
swaggerOpenApiSpec.paths[fullPath][method] = operation;
|
|
163
|
+
}
|
|
164
|
+
// Remove the rest route path from the rest api swagger
|
|
165
|
+
function removePathFromSwagger(path, method, swaggerOpenApiSpec) {
|
|
166
|
+
if (swaggerOpenApiSpec.paths?.[path]) {
|
|
167
|
+
delete swaggerOpenApiSpec.paths[path][method];
|
|
168
|
+
if (Object.keys(swaggerOpenApiSpec.paths[path]).length === 0) {
|
|
169
|
+
delete swaggerOpenApiSpec.paths[path];
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// Resolve a middleware descriptor to an actual Express RequestHandler
|
|
174
|
+
function resolveMiddleware(descriptor) {
|
|
175
|
+
switch (descriptor.name) {
|
|
176
|
+
case 'json':
|
|
177
|
+
return json(descriptor.options);
|
|
178
|
+
case 'urlencoded':
|
|
179
|
+
return urlencoded(descriptor.options);
|
|
180
|
+
case 'text':
|
|
181
|
+
return text(descriptor.options);
|
|
182
|
+
case 'raw':
|
|
183
|
+
return raw(descriptor.options);
|
|
184
|
+
case 'acl':
|
|
185
|
+
return acl(descriptor.acls);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// Build zod schema to allow easy input validation
|
|
189
|
+
function buildZodSchema(def) {
|
|
190
|
+
const shape = {};
|
|
191
|
+
for (const [key, field] of Object.entries(def)) {
|
|
192
|
+
let schema;
|
|
193
|
+
switch (field.type) {
|
|
194
|
+
case 'string':
|
|
195
|
+
schema = z.string();
|
|
196
|
+
break;
|
|
197
|
+
case 'boolean':
|
|
198
|
+
schema = z.boolean();
|
|
199
|
+
break;
|
|
200
|
+
case 'number':
|
|
201
|
+
schema = z.number();
|
|
202
|
+
break;
|
|
203
|
+
case 'enum':
|
|
204
|
+
schema = z.enum(field.enum);
|
|
205
|
+
break;
|
|
206
|
+
default:
|
|
207
|
+
throw new Error(`Unsupported type: ${field.type}`);
|
|
208
|
+
}
|
|
209
|
+
if (field.example)
|
|
210
|
+
schema = schema.meta({ example: field.example });
|
|
211
|
+
if (field.optional)
|
|
212
|
+
schema = schema.optional();
|
|
213
|
+
shape[key] = schema;
|
|
214
|
+
}
|
|
215
|
+
return z.object(shape);
|
|
216
|
+
}
|
|
217
|
+
// Build OpenApi parameters from zod schema
|
|
218
|
+
function extractParametersFromZod(schema, location) {
|
|
219
|
+
const { schema: jsonSchema } = createSchema(schema, { io: 'input' });
|
|
220
|
+
// If it’s a $ref, we can’t expand properties
|
|
221
|
+
if ('$ref' in jsonSchema) {
|
|
222
|
+
return [
|
|
223
|
+
{
|
|
224
|
+
name: location,
|
|
225
|
+
in: location,
|
|
226
|
+
required: true,
|
|
227
|
+
schema: jsonSchema,
|
|
228
|
+
},
|
|
229
|
+
];
|
|
230
|
+
}
|
|
231
|
+
// Otherwise, iterate properties
|
|
232
|
+
if (jsonSchema.properties) {
|
|
233
|
+
return Object.entries(jsonSchema.properties).map(([name, property]) => ({
|
|
234
|
+
name,
|
|
235
|
+
in: location,
|
|
236
|
+
required: (jsonSchema.required ?? []).includes(name),
|
|
237
|
+
schema: {
|
|
238
|
+
type: property.type,
|
|
239
|
+
},
|
|
240
|
+
example: property.example,
|
|
241
|
+
}));
|
|
242
|
+
}
|
|
243
|
+
return [];
|
|
244
|
+
}
|
|
245
|
+
function coerceBooleanQueryParams(query, def) {
|
|
246
|
+
if (!def)
|
|
247
|
+
return;
|
|
248
|
+
for (const [key, field] of Object.entries(def)) {
|
|
249
|
+
if (field.type === 'boolean' && typeof query[key] === 'string') {
|
|
250
|
+
query[key] = query[key] === 'true';
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// Exported for retro-compatibility
|
|
255
|
+
// TODO: remove export when RestApi.registerRestApi is no longer used
|
|
256
|
+
export async function sendObjects(iterable, req, res) {
|
|
257
|
+
const jsonFormat = !Object.hasOwn(req.query, 'ndjson');
|
|
258
|
+
res.setHeader('content-type', jsonFormat ? 'application/json' : 'application/x-ndjson');
|
|
259
|
+
return pipeline(Readable.from((jsonFormat ? makeJsonStream : makeNdJsonStream)(iterable)), res);
|
|
260
|
+
}
|
|
@@ -16,18 +16,21 @@ import { AlarmService } from '../alarms/alarm.service.mjs';
|
|
|
16
16
|
import { BASE_URL } from '../index.mjs';
|
|
17
17
|
import { escapeUnsafeComplexMatcher } from '../helpers/utils.helper.mjs';
|
|
18
18
|
import { genericAlarmsExample } from '../open-api/oa-examples/alarm.oa-example.mjs';
|
|
19
|
-
import { asynchronousActionResp, badRequestResp, createdResp, internalServerErrorResp, invalidParameters as invalidParametersResp,
|
|
19
|
+
import { asynchronousActionResp, badRequestResp, createdResp, forbiddenOperationResp, internalServerErrorResp, invalidParameters as invalidParametersResp, noContentResp, notFoundResp, unauthorizedResp, } from '../open-api/common/response.common.mjs';
|
|
20
20
|
import { partialSrs, sr, srIds } from '../open-api/oa-examples/sr.oa-example.mjs';
|
|
21
21
|
import { vdiId } from '../open-api/oa-examples/vdi.oa-example.mjs';
|
|
22
22
|
import { RestApi } from '../rest-api/rest-api.mjs';
|
|
23
23
|
import { XapiXoController } from '../abstract-classes/xapi-xo-controller.mjs';
|
|
24
24
|
import { messageIds, partialMessages } from '../open-api/oa-examples/message.oa-example.mjs';
|
|
25
25
|
import { taskIds, partialTasks, taskLocation } from '../open-api/oa-examples/task.oa-example.mjs';
|
|
26
|
+
import { SrService } from './sr.service.mjs';
|
|
26
27
|
let SrController = class SrController extends XapiXoController {
|
|
27
28
|
#alarmService;
|
|
28
|
-
|
|
29
|
+
#srService;
|
|
30
|
+
constructor(restApi, alarmService, srService) {
|
|
29
31
|
super('SR', restApi);
|
|
30
32
|
this.#alarmService = alarmService;
|
|
33
|
+
this.#srService = srService;
|
|
31
34
|
}
|
|
32
35
|
/**
|
|
33
36
|
* Returns all SRs that match the following privilege:
|
|
@@ -208,6 +211,15 @@ let SrController = class SrController extends XapiXoController {
|
|
|
208
211
|
},
|
|
209
212
|
});
|
|
210
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* Required privilege:
|
|
216
|
+
* - resource: sr, action: delete
|
|
217
|
+
*
|
|
218
|
+
* @example id "c4284e12-37c9-7967-b9e8-83ef229c3e03"
|
|
219
|
+
*/
|
|
220
|
+
async deleteSr(id) {
|
|
221
|
+
await this.#srService.delete(id);
|
|
222
|
+
}
|
|
211
223
|
};
|
|
212
224
|
__decorate([
|
|
213
225
|
Example(srIds),
|
|
@@ -338,6 +350,14 @@ __decorate([
|
|
|
338
350
|
__param(0, Path()),
|
|
339
351
|
__param(1, Query())
|
|
340
352
|
], SrController.prototype, "forgetSr", null);
|
|
353
|
+
__decorate([
|
|
354
|
+
Delete('{id}'),
|
|
355
|
+
Middlewares(acl({ resource: 'sr', action: 'delete', objectId: 'params.id' })),
|
|
356
|
+
SuccessResponse(noContentResp.status, noContentResp.description),
|
|
357
|
+
Response(forbiddenOperationResp.status, forbiddenOperationResp.description),
|
|
358
|
+
Response(notFoundResp.status, notFoundResp.description),
|
|
359
|
+
__param(0, Path())
|
|
360
|
+
], SrController.prototype, "deleteSr", null);
|
|
341
361
|
SrController = __decorate([
|
|
342
362
|
Route('srs'),
|
|
343
363
|
Security('*'),
|
|
@@ -346,6 +366,7 @@ SrController = __decorate([
|
|
|
346
366
|
Tags('srs'),
|
|
347
367
|
provide(SrController),
|
|
348
368
|
__param(0, inject(RestApi)),
|
|
349
|
-
__param(1, inject(AlarmService))
|
|
369
|
+
__param(1, inject(AlarmService)),
|
|
370
|
+
__param(2, inject(SrService))
|
|
350
371
|
], SrController);
|
|
351
372
|
export { SrController };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { LicenseService } from '../licenses/license.service.mjs';
|
|
2
|
+
export class SrService {
|
|
3
|
+
#restApi;
|
|
4
|
+
constructor(restApi) {
|
|
5
|
+
this.#restApi = restApi;
|
|
6
|
+
}
|
|
7
|
+
async delete(id) {
|
|
8
|
+
const sr = this.#restApi.getObject(id, 'SR');
|
|
9
|
+
const xapiSr = this.#restApi.getXapiObject(id, 'SR');
|
|
10
|
+
const xapi = xapiSr.$xapi;
|
|
11
|
+
if (sr.SR_type === 'linstor') {
|
|
12
|
+
const licenseService = this.#restApi.ioc.get(LicenseService);
|
|
13
|
+
const licenses = await licenseService.getXostorLicenses(id);
|
|
14
|
+
await Promise.all(licenses.map(({ licenseId, boundObjectId }) => this.#restApi.xoApp.unbindLicense({ licenseId, boundObjectId, productId: 'xostor' })));
|
|
15
|
+
try {
|
|
16
|
+
await xapi.xostor_delete(xapiSr.$ref);
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
await Promise.all(licenses.map(({ licenseId, boundObjectId }) => this.#restApi.xoApp.bindLicense({ licenseId, boundObjectId })));
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
await xapi.destroySr(id);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -108,7 +108,14 @@ let VifController = class VifController extends XapiXoController {
|
|
|
108
108
|
/**
|
|
109
109
|
* @example body {
|
|
110
110
|
* "networkId": "6b6ca0f5-6611-0636-4b0a-1fb1c1e96414",
|
|
111
|
-
* "vmId": "613f541c-4bed-fc77-7ca8-2db6b68f079c"
|
|
111
|
+
* "vmId": "613f541c-4bed-fc77-7ca8-2db6b68f079c",
|
|
112
|
+
* "other_config": {
|
|
113
|
+
*"ethtool-tx": "false"
|
|
114
|
+
* },
|
|
115
|
+
* "qos_algorithm_params": {
|
|
116
|
+
* "kbps": "42"
|
|
117
|
+
* },
|
|
118
|
+
* "qos_algorithm_type": "ratelimit"
|
|
112
119
|
* }
|
|
113
120
|
*/
|
|
114
121
|
async createVif(body) {
|
|
@@ -26,6 +26,7 @@ import { VmService } from './vm.service.mjs';
|
|
|
26
26
|
import { BackupJobService } from '../backup-jobs/backup-job.service.mjs';
|
|
27
27
|
import { partialVmBackupJobs, vmBackupJobIds } from '../open-api/oa-examples/backup-job.oa-example.mjs';
|
|
28
28
|
import { messageIds, partialMessages } from '../open-api/oa-examples/message.oa-example.mjs';
|
|
29
|
+
import { Task } from '@vates/task';
|
|
29
30
|
const IGNORED_VDIS_TAG = '[NOSNAP]';
|
|
30
31
|
let VmController = class VmController extends XapiXoController {
|
|
31
32
|
#vmService;
|
|
@@ -355,6 +356,40 @@ let VmController = class VmController extends XapiXoController {
|
|
|
355
356
|
},
|
|
356
357
|
});
|
|
357
358
|
}
|
|
359
|
+
/**
|
|
360
|
+
* Required privilege:
|
|
361
|
+
* - resource: vm, action: revert-snapshot
|
|
362
|
+
* - resource: vm, action: snapshot (if `snapshotBefore: true`)
|
|
363
|
+
*
|
|
364
|
+
* @example id "f07ab729-c0e8-721c-45ec-f11276377030"
|
|
365
|
+
* @example body { "snapshotId": "f07ab729-c0e8-721c-45ec-f11276377030", "snapshotBefore": true }
|
|
366
|
+
*/
|
|
367
|
+
async revertSnapshotVm(id, body, sync) {
|
|
368
|
+
const vmId = id;
|
|
369
|
+
const action = async () => {
|
|
370
|
+
const snapshotId = body.snapshotId;
|
|
371
|
+
// Ensure the snapshot belongs to this VM
|
|
372
|
+
const vm = this.getObject(vmId);
|
|
373
|
+
if (!vm.snapshots.includes(snapshotId)) {
|
|
374
|
+
throw invalidParameters(`snapshot ${snapshotId} does not belong to VM ${vmId}`);
|
|
375
|
+
}
|
|
376
|
+
if (body.snapshotBefore) {
|
|
377
|
+
await Task.run({ properties: { name: 'snapshot VM', objectId: vmId, objectType: 'VM' } }, () => this.getXapiObject(vmId).$snapshot({ ignoredVdisTag: IGNORED_VDIS_TAG }));
|
|
378
|
+
await Task.run({ properties: { name: 'revert snapshot', objectId: vmId, objectType: 'VM' } }, () => this.getXapi(vmId).revertVm(snapshotId));
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
await this.getXapi(vmId).revertVm(snapshotId);
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
return this.createAction(action, {
|
|
385
|
+
sync,
|
|
386
|
+
statusCode: noContentResp.status,
|
|
387
|
+
taskProperties: {
|
|
388
|
+
name: 'revert VM snapshot',
|
|
389
|
+
objectId: vmId,
|
|
390
|
+
},
|
|
391
|
+
});
|
|
392
|
+
}
|
|
358
393
|
/**
|
|
359
394
|
* Required privilege:
|
|
360
395
|
* - resource: vm, action: snapshot
|
|
@@ -778,6 +813,30 @@ __decorate([
|
|
|
778
813
|
__param(0, Path()),
|
|
779
814
|
__param(1, Query())
|
|
780
815
|
], VmController.prototype, "unpauseVm", null);
|
|
816
|
+
__decorate([
|
|
817
|
+
Example(taskLocation),
|
|
818
|
+
Post('{id}/actions/revert_snapshot'),
|
|
819
|
+
Middlewares([
|
|
820
|
+
json(),
|
|
821
|
+
acl([
|
|
822
|
+
{ resource: 'vm', action: 'revert-snapshot', objectId: 'params.id' },
|
|
823
|
+
{
|
|
824
|
+
resource: 'vm',
|
|
825
|
+
action: ({ req }) => (req.body.snapshotBefore ? 'snapshot' : undefined),
|
|
826
|
+
objectId: 'params.id',
|
|
827
|
+
},
|
|
828
|
+
]),
|
|
829
|
+
]),
|
|
830
|
+
SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description),
|
|
831
|
+
Response(noContentResp.status, noContentResp.description),
|
|
832
|
+
Response(forbiddenOperationResp.status, forbiddenOperationResp.description),
|
|
833
|
+
Response(notFoundResp.status, notFoundResp.description),
|
|
834
|
+
Response(invalidParametersResp.status, invalidParametersResp.description),
|
|
835
|
+
Response(internalServerErrorResp.status, internalServerErrorResp.description),
|
|
836
|
+
__param(0, Path()),
|
|
837
|
+
__param(1, Body()),
|
|
838
|
+
__param(2, Query())
|
|
839
|
+
], VmController.prototype, "revertSnapshotVm", null);
|
|
781
840
|
__decorate([
|
|
782
841
|
Example(taskLocation),
|
|
783
842
|
Post('{id}/actions/snapshot'),
|