@xen-orchestra/rest-api 0.24.0 → 0.25.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/abstract-classes/base-controller.mjs +4 -1
- package/dist/abstract-classes/xapi-xo-controller.mjs +4 -6
- package/dist/abstract-classes/xo-controller.mjs +4 -19
- package/dist/backup-archives/backup-archive.controller.mjs +7 -1
- package/dist/backup-jobs/backup-job.controller.mjs +2 -2
- package/dist/backup-logs/backup-log.controller.mjs +1 -1
- package/dist/backup-repositories/backup-repositories.controller.mjs +7 -1
- package/dist/events/event.class.mjs +31 -22
- package/dist/events/event.service.mjs +21 -4
- package/dist/groups/group.controller.mjs +1 -1
- package/dist/helpers/object-wrapper.helper.mjs +9 -4
- package/dist/middlewares/authentication.middleware.mjs +1 -1
- package/dist/open-api/oa-examples/vif.oa-example.mjs +1 -0
- package/dist/open-api/routes/routes.js +229 -5
- package/dist/pbds/pbd.controller.mjs +61 -2
- package/dist/pools/pool.controller.mjs +12 -3
- package/dist/pools/pool.service.mjs +3 -3
- package/dist/proxies/proxy.controller.mjs +7 -1
- package/dist/restore-logs/restore-log.controller.mjs +2 -2
- package/dist/schedules/schedule.controller.mjs +7 -1
- package/dist/servers/server.controller.mjs +7 -1
- package/dist/srs/sr.controller.mjs +29 -1
- package/dist/tasks/task.controller.mjs +7 -1
- package/dist/users/user.controller.mjs +1 -1
- package/dist/vdis/vdi.controller.mjs +30 -1
- package/dist/vifs/vif.controller.mjs +53 -3
- package/dist/vms/vm.controller.mjs +41 -1
- package/open-api/spec/swagger.json +822 -25
- package/package.json +4 -4
|
@@ -9,8 +9,10 @@ import { NDJSON_CONTENT_TYPE, safeParseComplexMatcher } from '../helpers/utils.h
|
|
|
9
9
|
const noop = () => { };
|
|
10
10
|
export class BaseController extends Controller {
|
|
11
11
|
restApi;
|
|
12
|
-
|
|
12
|
+
type;
|
|
13
|
+
constructor(type, restApi) {
|
|
13
14
|
super();
|
|
15
|
+
this.type = type;
|
|
14
16
|
this.restApi = restApi;
|
|
15
17
|
}
|
|
16
18
|
sendObjects(objects, req, path) {
|
|
@@ -51,6 +53,7 @@ export class BaseController extends Controller {
|
|
|
51
53
|
async createAction(cb, { statusCode = 200, sync = false, taskProperties, }) {
|
|
52
54
|
taskProperties.name = 'REST API: ' + taskProperties.name;
|
|
53
55
|
taskProperties.type = 'xo:rest-api:action';
|
|
56
|
+
taskProperties.objectType = this.type;
|
|
54
57
|
const task = this.restApi.tasks.create(taskProperties);
|
|
55
58
|
const pResult = task.run(() => cb(task));
|
|
56
59
|
if (sync) {
|
|
@@ -2,19 +2,17 @@ import { BaseController } from './base-controller.mjs';
|
|
|
2
2
|
import { escapeUnsafeComplexMatcher } from '../helpers/utils.helper.mjs';
|
|
3
3
|
import { RAW_ALARM_FILTER } from '../alarms/alarm.service.mjs';
|
|
4
4
|
export class XapiXoController extends BaseController {
|
|
5
|
-
#type;
|
|
6
5
|
constructor(type, restApi) {
|
|
7
|
-
super(restApi);
|
|
8
|
-
this.#type = type;
|
|
6
|
+
super(type, restApi);
|
|
9
7
|
}
|
|
10
8
|
getObjects(opts) {
|
|
11
|
-
return this.restApi.getObjectsByType(this
|
|
9
|
+
return this.restApi.getObjectsByType(this.type, opts);
|
|
12
10
|
}
|
|
13
11
|
getObject(id) {
|
|
14
|
-
return this.restApi.getObject(id, this
|
|
12
|
+
return this.restApi.getObject(id, this.type);
|
|
15
13
|
}
|
|
16
14
|
getXapiObject(maybeId) {
|
|
17
|
-
return this.restApi.getXapiObject(maybeId, this
|
|
15
|
+
return this.restApi.getXapiObject(maybeId, this.type);
|
|
18
16
|
}
|
|
19
17
|
getMessagesForObject(id, { filter, limit } = {}) {
|
|
20
18
|
const object = this.getObject(id);
|
|
@@ -1,19 +1,8 @@
|
|
|
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 { inject } from 'inversify';
|
|
11
1
|
import { BaseController } from './base-controller.mjs';
|
|
12
|
-
import { RestApi } from '../rest-api/rest-api.mjs';
|
|
13
2
|
import { limitAndFilterArray } from '../helpers/utils.helper.mjs';
|
|
14
|
-
|
|
15
|
-
constructor(restApi) {
|
|
16
|
-
super(restApi);
|
|
3
|
+
export class XoController extends BaseController {
|
|
4
|
+
constructor(type, restApi) {
|
|
5
|
+
super(type, restApi);
|
|
17
6
|
}
|
|
18
7
|
async getObjects(opts = {}) {
|
|
19
8
|
let objects = await this.getAllCollectionObjects(opts);
|
|
@@ -27,8 +16,4 @@ let XoController = class XoController extends BaseController {
|
|
|
27
16
|
async getObject(id) {
|
|
28
17
|
return this.getCollectionObject(id);
|
|
29
18
|
}
|
|
30
|
-
}
|
|
31
|
-
XoController = __decorate([
|
|
32
|
-
__param(0, inject(RestApi))
|
|
33
|
-
], XoController);
|
|
34
|
-
export { XoController };
|
|
19
|
+
}
|
|
@@ -8,14 +8,19 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
|
8
8
|
return function (target, key) { decorator(target, key, paramIndex); }
|
|
9
9
|
};
|
|
10
10
|
import { Example, Get, Path, Query, Request, Response, Route, Security, Tags } from 'tsoa';
|
|
11
|
+
import { inject } from 'inversify';
|
|
11
12
|
import { noSuchObject } from 'xo-common/api-errors.js';
|
|
12
13
|
import { provide } from 'inversify-binding-decorators';
|
|
13
14
|
import { badRequestResp, notFoundResp, unauthorizedResp } from '../open-api/common/response.common.mjs';
|
|
14
15
|
import { XoController } from '../abstract-classes/xo-controller.mjs';
|
|
16
|
+
import { RestApi } from '../rest-api/rest-api.mjs';
|
|
15
17
|
import { backupArchive, backupArchiveIds, partialBackupArchives, } from '../open-api/oa-examples/backup-archive.oa-example.mjs';
|
|
16
18
|
// BR uuid/xo-vm-backups/VM uuid/(ISO 8601 compact).json
|
|
17
19
|
const BACKUP_ARCHIVE_ID_REGEX = /^([0-9a-fA-F-]{36})\/+xo-vm-backups\/+([0-9a-fA-F-]{36})\/+(\d{8}T\d{6}Z)\.json$/;
|
|
18
20
|
let BackupArchiveController = class BackupArchiveController extends XoController {
|
|
21
|
+
constructor(restApi) {
|
|
22
|
+
super('backup-archive', restApi);
|
|
23
|
+
}
|
|
19
24
|
async getAllCollectionObjects({ backupRepositories = [], } = {}) {
|
|
20
25
|
const backupRepositoryIds = [];
|
|
21
26
|
if (backupRepositories.includes('*')) {
|
|
@@ -92,6 +97,7 @@ BackupArchiveController = __decorate([
|
|
|
92
97
|
Response(badRequestResp.status, badRequestResp.description),
|
|
93
98
|
Response(unauthorizedResp.status, unauthorizedResp.description),
|
|
94
99
|
Tags('backup-archives'),
|
|
95
|
-
provide(BackupArchiveController)
|
|
100
|
+
provide(BackupArchiveController),
|
|
101
|
+
__param(0, inject(RestApi))
|
|
96
102
|
], BackupArchiveController);
|
|
97
103
|
export { BackupArchiveController };
|
|
@@ -25,7 +25,7 @@ const log = createLogger('xo:rest-api:backupJob-controller');
|
|
|
25
25
|
let BackupJobController = class BackupJobController extends XoController {
|
|
26
26
|
#backupJobService;
|
|
27
27
|
constructor(restApi, backupJobService) {
|
|
28
|
-
super(restApi);
|
|
28
|
+
super('backup-job', restApi);
|
|
29
29
|
this.#backupJobService = backupJobService;
|
|
30
30
|
}
|
|
31
31
|
async getAllCollectionObjects() {
|
|
@@ -89,7 +89,7 @@ let DeprecatedBackupController = class DeprecatedBackupController extends XoCont
|
|
|
89
89
|
#backupLogService;
|
|
90
90
|
#backupJobService;
|
|
91
91
|
constructor(restApi, backupLogService, backupJobService) {
|
|
92
|
-
super(restApi);
|
|
92
|
+
super('backup', restApi);
|
|
93
93
|
this.#backupLogService = backupLogService;
|
|
94
94
|
this.#backupJobService = backupJobService;
|
|
95
95
|
}
|
|
@@ -19,7 +19,7 @@ import { XoController } from '../abstract-classes/xo-controller.mjs';
|
|
|
19
19
|
let BackupLogController = class BackupLogController extends XoController {
|
|
20
20
|
#backupLogService;
|
|
21
21
|
constructor(restApi, backupLogService) {
|
|
22
|
-
super(restApi);
|
|
22
|
+
super('backup-log', restApi);
|
|
23
23
|
this.#backupLogService = backupLogService;
|
|
24
24
|
}
|
|
25
25
|
getAllCollectionObjects() {
|
|
@@ -8,11 +8,16 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
|
8
8
|
return function (target, key) { decorator(target, key, paramIndex); }
|
|
9
9
|
};
|
|
10
10
|
import { Example, Get, Path, Query, Request, Response, Route, Security, Tags } from 'tsoa';
|
|
11
|
+
import { inject } from 'inversify';
|
|
11
12
|
import { provide } from 'inversify-binding-decorators';
|
|
12
13
|
import { badRequestResp, notFoundResp, unauthorizedResp } from '../open-api/common/response.common.mjs';
|
|
13
14
|
import { backupRepositoryIds, partialBackupRepositories, backupRepository, } from '../open-api/oa-examples/backup-repository.oa-example.mjs';
|
|
14
15
|
import { XoController } from '../abstract-classes/xo-controller.mjs';
|
|
16
|
+
import { RestApi } from '../rest-api/rest-api.mjs';
|
|
15
17
|
let BackupRepositoryController = class BackupRepositoryController extends XoController {
|
|
18
|
+
constructor(restApi) {
|
|
19
|
+
super('backup-repository', restApi);
|
|
20
|
+
}
|
|
16
21
|
// --- abstract methods
|
|
17
22
|
getAllCollectionObjects() {
|
|
18
23
|
return this.restApi.xoApp.getAllRemotes();
|
|
@@ -57,6 +62,7 @@ BackupRepositoryController = __decorate([
|
|
|
57
62
|
Response(badRequestResp.status, badRequestResp.description),
|
|
58
63
|
Response(unauthorizedResp.status, unauthorizedResp.description),
|
|
59
64
|
Tags('backup-repositories'),
|
|
60
|
-
provide(BackupRepositoryController)
|
|
65
|
+
provide(BackupRepositoryController),
|
|
66
|
+
__param(0, inject(RestApi))
|
|
61
67
|
], BackupRepositoryController);
|
|
62
68
|
export { BackupRepositoryController };
|
|
@@ -19,31 +19,35 @@ export class Subscriber {
|
|
|
19
19
|
get connection() {
|
|
20
20
|
return this.#connection;
|
|
21
21
|
}
|
|
22
|
-
constructor(
|
|
22
|
+
constructor(connection, manager) {
|
|
23
23
|
this.#id = crypto.randomUUID();
|
|
24
|
-
|
|
25
|
-
'Content-Type': 'text/event-stream',
|
|
26
|
-
Connection: 'keep-alive',
|
|
27
|
-
'Cache-Control': 'no-cache, no-transform',
|
|
28
|
-
});
|
|
29
|
-
res.setHeaders(headers);
|
|
30
|
-
res.on('close', () => this.clear());
|
|
24
|
+
connection.on('close', () => this.clear());
|
|
31
25
|
manager.addSubscriber(this);
|
|
32
|
-
this.#connection =
|
|
26
|
+
this.#connection = connection;
|
|
33
27
|
this.#manager = manager;
|
|
34
28
|
this.#isAlive = true;
|
|
35
29
|
}
|
|
30
|
+
#safeWrite(payload) {
|
|
31
|
+
const ok = this.#connection.write(payload);
|
|
32
|
+
if (!ok) {
|
|
33
|
+
log.error(`Too much data in queue for the client ${this.id} (${Math.round(this.#connection.writableLength / 1024 / 1024)} MB). The connection is going to be destroyed`);
|
|
34
|
+
this.clear();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
36
37
|
broadcast(event, data) {
|
|
37
38
|
if (!this.#isAlive) {
|
|
38
39
|
log.warn('broadcast called on a subscriber that is not alive, but still in memory! Force clear and do nothing');
|
|
39
40
|
this.clear();
|
|
40
41
|
return;
|
|
41
42
|
}
|
|
42
|
-
|
|
43
|
-
this.#
|
|
43
|
+
const payload = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
44
|
+
this.#safeWrite(payload);
|
|
44
45
|
}
|
|
45
46
|
clear() {
|
|
46
47
|
this.#isAlive = false;
|
|
48
|
+
if (!this.#connection.closed || !this.#connection.destroyed) {
|
|
49
|
+
this.#connection.destroy();
|
|
50
|
+
}
|
|
47
51
|
this.#manager.removeSubscriber(this.id);
|
|
48
52
|
}
|
|
49
53
|
}
|
|
@@ -58,20 +62,25 @@ export class XoListener extends Listener {
|
|
|
58
62
|
handleData({ fields, event }, object, previousObj) {
|
|
59
63
|
let _object = object;
|
|
60
64
|
let _prevObject = previousObj;
|
|
61
|
-
if (this.#type === 'alarm') {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
if (this.#type === 'alarm' || this.#type === 'message') {
|
|
66
|
+
const isAlarm = (object) => object !== undefined && 'type' in object && object.type === 'message' && this.#alarmService.isAlarm(object);
|
|
67
|
+
const objectIsAlarm = isAlarm(object);
|
|
68
|
+
const prevObjectIsAlarm = isAlarm(previousObj);
|
|
69
|
+
// If we are in an alarm listener and the objects are messages
|
|
70
|
+
// we clean them to ensure they are not sent via the SSE
|
|
71
|
+
// Same if we are in a message listener and the objects are alarms
|
|
72
|
+
if (this.#type === 'alarm') {
|
|
73
|
+
_object = objectIsAlarm ? this.#alarmService.parseAlarm(object) : undefined;
|
|
74
|
+
_prevObject = prevObjectIsAlarm ? this.#alarmService.parseAlarm(previousObj) : undefined;
|
|
67
75
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
this.#alarmService?.isAlarm(previousObj)) {
|
|
72
|
-
_prevObject = this.#alarmService.parseAlarm(previousObj);
|
|
76
|
+
else {
|
|
77
|
+
_object = objectIsAlarm ? undefined : object;
|
|
78
|
+
_prevObject = prevObjectIsAlarm ? undefined : object;
|
|
73
79
|
}
|
|
74
80
|
}
|
|
81
|
+
if (_object === undefined && _prevObject === undefined) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
75
84
|
if (fields !== '*') {
|
|
76
85
|
if (_object !== undefined) {
|
|
77
86
|
_object = pick(_object, fields);
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
1
2
|
import { createLogger } from '@xen-orchestra/log';
|
|
3
|
+
import { PassThrough, pipeline } from 'node:stream';
|
|
2
4
|
import { PingListener, Subscriber, SubscriberManager, XoListener } from './event.class.mjs';
|
|
3
5
|
import { AlarmService } from '../alarms/alarm.service.mjs';
|
|
4
6
|
const log = createLogger('xo:rest-api:event-service');
|
|
@@ -26,7 +28,7 @@ export class EventService {
|
|
|
26
28
|
listener = new PingListener();
|
|
27
29
|
}
|
|
28
30
|
else {
|
|
29
|
-
const
|
|
31
|
+
const isMessage = type === 'alarm' || type === 'message';
|
|
30
32
|
let eventEmitter;
|
|
31
33
|
if (type === 'task') {
|
|
32
34
|
eventEmitter = this.#restApi.xoApp.tasks;
|
|
@@ -34,15 +36,30 @@ export class EventService {
|
|
|
34
36
|
else {
|
|
35
37
|
// alarm is purely XO-related; it doesn't exist at the XAPI level.
|
|
36
38
|
// alarm is a message with parsed values. So, in the case of an alarm listener, it listens for message collection.
|
|
37
|
-
eventEmitter = this.#restApi.xoApp.objects.allIndexes.type.getEventEmitterByType(
|
|
39
|
+
eventEmitter = this.#restApi.xoApp.objects.allIndexes.type.getEventEmitterByType(isMessage ? 'message' : type);
|
|
38
40
|
}
|
|
39
|
-
listener = new XoListener(type, eventEmitter,
|
|
41
|
+
listener = new XoListener(type, eventEmitter, isMessage ? this.#alarmService : undefined);
|
|
40
42
|
}
|
|
41
43
|
this.#listeners.set(type, listener);
|
|
42
44
|
return listener;
|
|
43
45
|
}
|
|
44
46
|
createSseSubscriber(res) {
|
|
45
|
-
const
|
|
47
|
+
const headers = new Headers({
|
|
48
|
+
'Content-Type': 'text/event-stream',
|
|
49
|
+
Connection: 'keep-alive',
|
|
50
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
51
|
+
});
|
|
52
|
+
res.setHeaders(headers);
|
|
53
|
+
const maxRam = this.#restApi.xoApp.config.get('rest-api.percentOfRamAllocatedPerSseClient');
|
|
54
|
+
const connection = new PassThrough({
|
|
55
|
+
highWaterMark: Math.round(os.totalmem() * (maxRam / 100)),
|
|
56
|
+
});
|
|
57
|
+
pipeline(connection, res, error => {
|
|
58
|
+
if (error?.code !== 'ERR_STREAM_PREMATURE_CLOSE') {
|
|
59
|
+
log.error(error);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
const subscriber = new Subscriber(connection, this.#subscriberManager);
|
|
46
63
|
subscriber.broadcast('init', { id: subscriber.id });
|
|
47
64
|
this.addListenerFor(subscriber.id, { type: 'ping' });
|
|
48
65
|
log.debug(`new SSE subscriber added: ${subscriber.id}`);
|
|
@@ -23,7 +23,7 @@ import { partialTasks, taskIds } from '../open-api/oa-examples/task.oa-example.m
|
|
|
23
23
|
let GroupController = class GroupController extends XoController {
|
|
24
24
|
#userService;
|
|
25
25
|
constructor(restApi, userService) {
|
|
26
|
-
super(restApi);
|
|
26
|
+
super('group', restApi);
|
|
27
27
|
this.#userService = userService;
|
|
28
28
|
}
|
|
29
29
|
// --- abstract methods
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
1
|
import pick from 'lodash/pick.js';
|
|
3
2
|
import { BASE_URL } from '../index.mjs';
|
|
4
|
-
const { join } = path.posix;
|
|
5
3
|
export function makeObjectMapper(req, path) {
|
|
6
4
|
const makeUrl = (obj) => {
|
|
7
5
|
let _path;
|
|
@@ -9,9 +7,16 @@ export function makeObjectMapper(req, path) {
|
|
|
9
7
|
_path = req.path;
|
|
10
8
|
}
|
|
11
9
|
else {
|
|
12
|
-
|
|
10
|
+
let tmpPath = typeof path === 'string' ? path : path(obj);
|
|
11
|
+
if (tmpPath.startsWith('/')) {
|
|
12
|
+
tmpPath = tmpPath.slice(1);
|
|
13
|
+
}
|
|
14
|
+
if (tmpPath.endsWith('/')) {
|
|
15
|
+
tmpPath = tmpPath.slice(0, -1);
|
|
16
|
+
}
|
|
17
|
+
_path = `${BASE_URL}/${tmpPath}`;
|
|
13
18
|
}
|
|
14
|
-
return
|
|
19
|
+
return `${_path}/${String(obj.id)}`;
|
|
15
20
|
};
|
|
16
21
|
let objectMapper;
|
|
17
22
|
const { query } = req;
|
|
@@ -51,7 +51,7 @@ export function setupApiContext(xoApp) {
|
|
|
51
51
|
res.locals.authType = 'basic';
|
|
52
52
|
}
|
|
53
53
|
try {
|
|
54
|
-
const { user } = await xoApp.authenticateUser(credentials, { ip });
|
|
54
|
+
const { user } = await xoApp.authenticateUser(credentials, { ip }, { bypassTaskCreation: hasToken });
|
|
55
55
|
return xoApp.runWithApiContext(user, next);
|
|
56
56
|
}
|
|
57
57
|
catch (error) {
|