@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.
@@ -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
- constructor(restApi) {
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.#type, opts);
9
+ return this.restApi.getObjectsByType(this.type, opts);
12
10
  }
13
11
  getObject(id) {
14
- return this.restApi.getObject(id, this.#type);
12
+ return this.restApi.getObject(id, this.type);
15
13
  }
16
14
  getXapiObject(maybeId) {
17
- return this.restApi.getXapiObject(maybeId, this.#type);
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
- let XoController = class XoController extends BaseController {
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(res, manager) {
22
+ constructor(connection, manager) {
23
23
  this.#id = crypto.randomUUID();
24
- const headers = new Headers({
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 = res;
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
- this.#connection.write(`event:${event}\n`);
43
- this.#connection.write(`data:${JSON.stringify(data)}\n\n`);
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
- if (object !== undefined &&
63
- 'type' in object &&
64
- object.type === 'message' &&
65
- this.#alarmService?.isAlarm(object)) {
66
- _object = this.#alarmService.parseAlarm(object);
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
- if (previousObj !== undefined &&
69
- 'type' in previousObj &&
70
- previousObj.type === 'message' &&
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 isAlarm = type === 'alarm';
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(isAlarm ? 'message' : type);
39
+ eventEmitter = this.#restApi.xoApp.objects.allIndexes.type.getEventEmitterByType(isMessage ? 'message' : type);
38
40
  }
39
- listener = new XoListener(type, eventEmitter, isAlarm ? this.#alarmService : undefined);
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 subscriber = new Subscriber(res, this.#subscriberManager);
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
- _path = `${BASE_URL}/${typeof path === 'string' ? path : path(obj)}`;
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 join(_path, typeof obj.id === 'number' ? String(obj.id) : obj.id);
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) {
@@ -2,6 +2,7 @@ export const vifIds = [
2
2
  '/rest/v0/vifs/f028c5d4-578a-332c-394e-087aaca32dd3',
3
3
  '/rest/v0/vifs/9cc245bf-8dac-8550-e1ae-54bc679b68d9',
4
4
  ];
5
+ export const vifId = { id: 'f028c5d4-578a-332c-394e-087aaca32dd3' };
5
6
  export const partialVifs = [
6
7
  {
7
8
  attached: true,