@xen-orchestra/rest-api 0.20.0 → 0.21.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 +2 -2
- package/dist/abstract-classes/listener.mjs +59 -0
- package/dist/backup-jobs/backup-job.controller.mjs +18 -8
- package/dist/backup-jobs/backup-job.service.mjs +28 -2
- package/dist/backup-logs/backup-log.service.mjs +8 -1
- package/dist/events/event.class.mjs +127 -0
- package/dist/events/event.controller.mjs +101 -0
- package/dist/events/event.service.mjs +53 -0
- package/dist/events/event.type.mjs +1 -0
- package/dist/ioc/ioc.mjs +8 -0
- package/dist/open-api/common/response.common.mjs +0 -1
- package/dist/open-api/oa-examples/backup-archive.oa-example.mjs +2 -1
- package/dist/open-api/oa-examples/event.oa-example.mjs +3 -0
- package/dist/open-api/oa-examples/task.oa-example.mjs +1 -1
- package/dist/open-api/oa-examples/vm.oa-example.mjs +74 -0
- package/dist/open-api/routes/routes.js +195 -21
- package/dist/pools/pool.controller.mjs +5 -5
- package/dist/schedules/schedule.controller.mjs +1 -1
- package/dist/servers/server.controller.mjs +2 -2
- package/dist/tasks/task.controller.mjs +1 -1
- package/dist/vms/vm.controller.mjs +44 -23
- package/dist/vms/vm.service.mjs +181 -0
- package/dist/vms/vm.type.mjs +1 -0
- package/dist/xoa/xoa.service.mjs +4 -1
- package/open-api/spec/swagger.json +939 -197
- package/package.json +4 -3
|
@@ -64,8 +64,8 @@ export class BaseController extends Controller {
|
|
|
64
64
|
const location = `${BASE_URL}/tasks/${task.id}`;
|
|
65
65
|
this.setStatus(202);
|
|
66
66
|
this.setHeader('Location', location);
|
|
67
|
-
this.setHeader('Content-Type', '
|
|
68
|
-
return
|
|
67
|
+
this.setHeader('Content-Type', 'application/json');
|
|
68
|
+
return { taskId: task.id };
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
getXapi(maybeId) {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export class Listener {
|
|
2
|
+
#subscribers = new Map();
|
|
3
|
+
#eventEmitter;
|
|
4
|
+
#eventCallbacks = new Map();
|
|
5
|
+
#watchedEvent;
|
|
6
|
+
constructor(eventEmitter, watchedEvent) {
|
|
7
|
+
this.#eventEmitter = eventEmitter;
|
|
8
|
+
this.#watchedEvent = watchedEvent;
|
|
9
|
+
}
|
|
10
|
+
get subscribers() {
|
|
11
|
+
return this.#subscribers;
|
|
12
|
+
}
|
|
13
|
+
get eventEmitter() {
|
|
14
|
+
return this.#eventEmitter;
|
|
15
|
+
}
|
|
16
|
+
addSubscriber(subscriber, fields = '*') {
|
|
17
|
+
this.#subscribers.set(subscriber.id, { fields, subscriber });
|
|
18
|
+
if (this.#subscribers.size === 1) {
|
|
19
|
+
this.#watchedEvent.forEach(event => this.#addEventListener(event));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
removeSubscriber(subscriberId) {
|
|
23
|
+
this.#subscribers.delete(subscriberId);
|
|
24
|
+
if (this.#subscribers.size === 0) {
|
|
25
|
+
this.removeAllEventListeners();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
removeAllEventListeners() {
|
|
29
|
+
this.#eventCallbacks.forEach((cb, event) => {
|
|
30
|
+
this.#eventEmitter.off(event, cb);
|
|
31
|
+
});
|
|
32
|
+
this.#eventCallbacks.clear();
|
|
33
|
+
}
|
|
34
|
+
clear() {
|
|
35
|
+
this.removeAllEventListeners();
|
|
36
|
+
this.#subscribers.clear();
|
|
37
|
+
this.#watchedEvent = [];
|
|
38
|
+
}
|
|
39
|
+
#addEventListener(event) {
|
|
40
|
+
if (this.#eventCallbacks.has(event)) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const broadcastAllSubscriber = (...args) => {
|
|
44
|
+
this.#subscribers.forEach(conf => {
|
|
45
|
+
if (!conf.subscriber.isAlive) {
|
|
46
|
+
this.removeSubscriber(conf.subscriber.id);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const data = this.handleData({ ...conf, event }, ...args);
|
|
50
|
+
if (data === undefined) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
conf.subscriber.broadcast(event, data);
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
this.#eventCallbacks.set(event, broadcastAllSubscriber);
|
|
57
|
+
this.#eventEmitter.on(event, broadcastAllSubscriber);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -21,17 +21,22 @@ import { limitAndFilterArray } from '../helpers/utils.helper.mjs';
|
|
|
21
21
|
import { XoController } from '../abstract-classes/xo-controller.mjs';
|
|
22
22
|
import { metadataBackupJob, metadataBackupJobIds, mirrorBackupJob, mirrorBackupJobIds, partialMetadataBackupJobs, partialMirrorBackupJobs, partialVmBackupJobs, vmBackupJob, vmBackupJobIds, } from '../open-api/oa-examples/backup-job.oa-example.mjs';
|
|
23
23
|
import { BASE_URL } from '../index.mjs';
|
|
24
|
+
import { BackupJobService } from './backup-job.service.mjs';
|
|
24
25
|
const log = createLogger('xo:rest-api:backupJob-controller');
|
|
25
26
|
let BackupJobController = class BackupJobController extends XoController {
|
|
27
|
+
#backupJobService;
|
|
28
|
+
constructor(restApi, backupJobService) {
|
|
29
|
+
super(restApi);
|
|
30
|
+
this.#backupJobService = backupJobService;
|
|
31
|
+
}
|
|
26
32
|
async getAllCollectionObjects() {
|
|
27
33
|
const allJobs = await this.restApi.xoApp.getAllJobs();
|
|
28
|
-
const backupJobs = allJobs.filter(job =>
|
|
34
|
+
const backupJobs = allJobs.filter(job => this.#backupJobService.isBackupJob(job));
|
|
29
35
|
return backupJobs;
|
|
30
36
|
}
|
|
31
37
|
async getCollectionObject(id) {
|
|
32
38
|
const job = await this.restApi.xoApp.getJob(id);
|
|
33
|
-
if (!(
|
|
34
|
-
// not a backup job
|
|
39
|
+
if (!this.#backupJobService.isBackupJob(job)) {
|
|
35
40
|
throw noSuchObject(id, 'backup-job');
|
|
36
41
|
}
|
|
37
42
|
return job;
|
|
@@ -75,23 +80,27 @@ BackupJobController = __decorate([
|
|
|
75
80
|
Response(badRequestResp.status, badRequestResp.description),
|
|
76
81
|
Response(unauthorizedResp.status, unauthorizedResp.description),
|
|
77
82
|
Tags('backup-jobs'),
|
|
78
|
-
provide(BackupJobController)
|
|
83
|
+
provide(BackupJobController),
|
|
84
|
+
__param(0, inject(RestApi)),
|
|
85
|
+
__param(1, inject(BackupJobService))
|
|
79
86
|
], BackupJobController);
|
|
80
87
|
export { BackupJobController };
|
|
81
88
|
// ----------- DEPRECATED TO BE REMOVED IN ONE YEAR (09-12-2026)--------------------
|
|
82
89
|
let DeprecatedBackupController = class DeprecatedBackupController extends XoController {
|
|
83
90
|
#backupLogService;
|
|
84
|
-
|
|
91
|
+
#backupJobService;
|
|
92
|
+
constructor(restApi, backupLogService, backupJobService) {
|
|
85
93
|
super(restApi);
|
|
86
94
|
this.#backupLogService = backupLogService;
|
|
95
|
+
this.#backupJobService = backupJobService;
|
|
87
96
|
}
|
|
88
97
|
async getAllCollectionObjects() {
|
|
89
98
|
const backupJobs = await this.restApi.xoApp.getAllJobs();
|
|
90
|
-
return backupJobs.filter(job =>
|
|
99
|
+
return backupJobs.filter(job => this.#backupJobService.isBackupJob(job));
|
|
91
100
|
}
|
|
92
101
|
async getCollectionObject(id) {
|
|
93
102
|
const backupJob = await this.restApi.xoApp.getJob(id);
|
|
94
|
-
if (!(
|
|
103
|
+
if (!this.#backupJobService.isBackupJob(backupJob)) {
|
|
95
104
|
throw noSuchObject(id, 'backup-job');
|
|
96
105
|
}
|
|
97
106
|
return backupJob;
|
|
@@ -280,7 +289,8 @@ DeprecatedBackupController = __decorate([
|
|
|
280
289
|
}),
|
|
281
290
|
provide(DeprecatedBackupController),
|
|
282
291
|
__param(0, inject(RestApi)),
|
|
283
|
-
__param(1, inject(BackupLogService))
|
|
292
|
+
__param(1, inject(BackupLogService)),
|
|
293
|
+
__param(2, inject(BackupJobService))
|
|
284
294
|
], DeprecatedBackupController);
|
|
285
295
|
export { DeprecatedBackupController };
|
|
286
296
|
// ----------- DEPRECATED TO BE REMOVED IN ONE YEAR (09-12-2026)--------------------
|
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import { createPredicate } from 'value-matcher';
|
|
2
2
|
import { extractIdsFromSimplePattern } from '@xen-orchestra/backups/extractIdsFromSimplePattern.mjs';
|
|
3
|
-
|
|
3
|
+
import { noSuchObject } from 'xo-common/api-errors.js';
|
|
4
|
+
import { vmContainsNoBakTag } from '../helpers/utils.helper.mjs';
|
|
4
5
|
export class BackupJobService {
|
|
6
|
+
#backupJobTypes = ['backup', 'metadataBackup', 'mirrorBackup'];
|
|
5
7
|
#restApi;
|
|
6
8
|
constructor(restApi) {
|
|
7
9
|
this.#restApi = restApi;
|
|
8
10
|
}
|
|
11
|
+
isBackupJob(anyJob) {
|
|
12
|
+
return this.#backupJobTypes.includes(anyJob.type);
|
|
13
|
+
}
|
|
9
14
|
async isVmInBackupJob(backupJobId, vmId) {
|
|
10
15
|
const backupJob = await this.#restApi.xoApp.getJob(backupJobId);
|
|
11
16
|
const vm = this.#restApi.getObject(vmId, 'VM');
|
|
12
|
-
if (vm
|
|
17
|
+
if (vmContainsNoBakTag(vm)) {
|
|
13
18
|
return false;
|
|
14
19
|
}
|
|
15
20
|
try {
|
|
@@ -22,4 +27,25 @@ export class BackupJobService {
|
|
|
22
27
|
return predicate(vm);
|
|
23
28
|
}
|
|
24
29
|
}
|
|
30
|
+
async backupJobHasAtLeastOneScheduleEnabled(id) {
|
|
31
|
+
const backupJob = await this.#restApi.xoApp.getJob(id);
|
|
32
|
+
for (const maybeScheduleId in backupJob.settings) {
|
|
33
|
+
if (maybeScheduleId === '') {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const schedule = await this.#restApi.xoApp.getSchedule(maybeScheduleId);
|
|
38
|
+
if (schedule.enabled) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (!noSuchObject.is(error, { id: maybeScheduleId, type: 'schedule' })) {
|
|
44
|
+
console.error(error);
|
|
45
|
+
}
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
25
51
|
}
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
export class BackupLogService {
|
|
2
2
|
isBackupLog(log) {
|
|
3
|
-
return log.message === 'backup'
|
|
3
|
+
return log.message === 'backup';
|
|
4
|
+
}
|
|
5
|
+
getVmBackupTaskLog(log, vmId) {
|
|
6
|
+
return log.tasks?.find(task => task.data?.id === vmId);
|
|
7
|
+
}
|
|
8
|
+
isVmInBackupLog(log, vmId) {
|
|
9
|
+
const vmIds = (log.infos?.find(info => info.message === 'vms')?.data).vms ?? [];
|
|
10
|
+
return vmIds.includes(vmId);
|
|
4
11
|
}
|
|
5
12
|
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import pick from 'lodash/pick.js';
|
|
2
|
+
import isEqual from 'lodash/isEqual.js';
|
|
3
|
+
import { createLogger } from '@xen-orchestra/log';
|
|
4
|
+
import { EventEmitter } from 'node:events';
|
|
5
|
+
import { noSuchObject } from 'xo-common/api-errors.js';
|
|
6
|
+
import { Listener } from '../abstract-classes/listener.mjs';
|
|
7
|
+
const log = createLogger('xo:rest-api:event-service');
|
|
8
|
+
export class Subscriber {
|
|
9
|
+
#id;
|
|
10
|
+
#manager;
|
|
11
|
+
#connection;
|
|
12
|
+
#isAlive;
|
|
13
|
+
get id() {
|
|
14
|
+
return this.#id;
|
|
15
|
+
}
|
|
16
|
+
get isAlive() {
|
|
17
|
+
return this.#isAlive;
|
|
18
|
+
}
|
|
19
|
+
get connection() {
|
|
20
|
+
return this.#connection;
|
|
21
|
+
}
|
|
22
|
+
constructor(res, manager) {
|
|
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());
|
|
31
|
+
manager.addSubscriber(this);
|
|
32
|
+
this.#connection = res;
|
|
33
|
+
this.#manager = manager;
|
|
34
|
+
this.#isAlive = true;
|
|
35
|
+
}
|
|
36
|
+
broadcast(event, data) {
|
|
37
|
+
if (!this.#isAlive) {
|
|
38
|
+
log.warn('broadcast called on a subscriber that is not alive, but still in memory! Force clear and do nothing');
|
|
39
|
+
this.clear();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
this.#connection.write(`event:${event}\n`);
|
|
43
|
+
this.#connection.write(`data:${JSON.stringify(data)}\n\n`);
|
|
44
|
+
}
|
|
45
|
+
clear() {
|
|
46
|
+
this.#isAlive = false;
|
|
47
|
+
this.#manager.removeSubscriber(this.id);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export class XapiXoListener extends Listener {
|
|
51
|
+
#type;
|
|
52
|
+
#alarmService;
|
|
53
|
+
constructor(type, eventEmitter, alarmService) {
|
|
54
|
+
super(eventEmitter, ['add', 'update', 'remove']);
|
|
55
|
+
this.#type = type;
|
|
56
|
+
this.#alarmService = alarmService;
|
|
57
|
+
}
|
|
58
|
+
handleData({ fields, event }, object, previousObj) {
|
|
59
|
+
let _object = object;
|
|
60
|
+
let _prevObject = previousObj;
|
|
61
|
+
if (this.#type === 'alarm') {
|
|
62
|
+
if (object?.type === 'message' && this.#alarmService?.isAlarm(object)) {
|
|
63
|
+
_object = this.#alarmService.parseAlarm(object);
|
|
64
|
+
}
|
|
65
|
+
if (previousObj?.type === 'message' && this.#alarmService?.isAlarm(previousObj)) {
|
|
66
|
+
_prevObject = this.#alarmService.parseAlarm(previousObj);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (fields !== '*') {
|
|
70
|
+
if (_object !== undefined) {
|
|
71
|
+
_object = pick(_object, fields);
|
|
72
|
+
}
|
|
73
|
+
if (_prevObject !== undefined) {
|
|
74
|
+
_prevObject = pick(_prevObject, fields);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (event === 'update' && fields !== '*' && isEqual(_object, _prevObject)) {
|
|
78
|
+
// if no changes from user perspective, don't send update
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
// if _object === undefined, this means we are on a remove event, so _prevObject will not be undefined
|
|
82
|
+
return _object ?? _prevObject;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export class PingListener extends Listener {
|
|
86
|
+
#intervalId;
|
|
87
|
+
constructor() {
|
|
88
|
+
super(new EventEmitter(), ['ping']);
|
|
89
|
+
this.#intervalId = setInterval(() => {
|
|
90
|
+
this.eventEmitter.emit('ping');
|
|
91
|
+
}, 1000 * 30);
|
|
92
|
+
}
|
|
93
|
+
handleData() {
|
|
94
|
+
return { ping: Date.now() };
|
|
95
|
+
}
|
|
96
|
+
clear() {
|
|
97
|
+
clearInterval(this.#intervalId);
|
|
98
|
+
super.clear();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
export class SubscriberManager {
|
|
102
|
+
#subscribers = new Map();
|
|
103
|
+
get subscribers() {
|
|
104
|
+
return this.#subscribers;
|
|
105
|
+
}
|
|
106
|
+
getSubscriber(id) {
|
|
107
|
+
const subscriber = this.#subscribers.get(id);
|
|
108
|
+
if (subscriber === undefined) {
|
|
109
|
+
throw noSuchObject(id, 'event');
|
|
110
|
+
}
|
|
111
|
+
return subscriber;
|
|
112
|
+
}
|
|
113
|
+
addSubscriber(subscriber) {
|
|
114
|
+
this.#subscribers.set(subscriber.id, subscriber);
|
|
115
|
+
}
|
|
116
|
+
removeSubscriber(id) {
|
|
117
|
+
this.#subscribers.delete(id);
|
|
118
|
+
}
|
|
119
|
+
clear() {
|
|
120
|
+
this.#subscribers.forEach(subscriber => {
|
|
121
|
+
if (!subscriber.connection.closed) {
|
|
122
|
+
subscriber.connection.destroy();
|
|
123
|
+
}
|
|
124
|
+
this.#subscribers.delete(subscriber.id);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
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 { Body, Controller, Example, Delete, Get, Middlewares, Path, Post, Request, Response, Route, Security, SuccessResponse, Tags, } from 'tsoa';
|
|
11
|
+
import { badRequestResp, createdResp, noContentResp, notFoundResp, unauthorizedResp, } from '../open-api/common/response.common.mjs';
|
|
12
|
+
import { provide } from 'inversify-binding-decorators';
|
|
13
|
+
import { inject } from 'inversify';
|
|
14
|
+
import { EventService } from './event.service.mjs';
|
|
15
|
+
import { json } from 'express';
|
|
16
|
+
import { addSubscription } from '../open-api/oa-examples/event.oa-example.mjs';
|
|
17
|
+
let EventController = class EventController extends Controller {
|
|
18
|
+
#eventService;
|
|
19
|
+
constructor(eventService) {
|
|
20
|
+
super();
|
|
21
|
+
this.#eventService = eventService;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Opens an SSE (Server-Sent Events) connection.
|
|
25
|
+
*
|
|
26
|
+
* By default, there are no active subscriptions in the stream.
|
|
27
|
+
* To add subscriptions, use the following endpoint:
|
|
28
|
+
*
|
|
29
|
+
* POST /rest/v0/events/:id/subscriptions
|
|
30
|
+
*
|
|
31
|
+
*
|
|
32
|
+
* Events you will receive:
|
|
33
|
+
* - **init**: The first event you will receive.
|
|
34
|
+
* Data: the connection ID.
|
|
35
|
+
*
|
|
36
|
+
* - **ping**: A simple event used to keep the connection alive between the server and the client.
|
|
37
|
+
* Data: the event timestamp.
|
|
38
|
+
*
|
|
39
|
+
* - **add**: Triggered when an object has been added.
|
|
40
|
+
* Data: the added object.
|
|
41
|
+
*
|
|
42
|
+
* - **update**: Triggered when an object has been updated.
|
|
43
|
+
* Data: the updated object.
|
|
44
|
+
*
|
|
45
|
+
* - **remove**: Triggered when an object has been removed.
|
|
46
|
+
* Data: the removed object.
|
|
47
|
+
*/
|
|
48
|
+
openSseConnection(req) {
|
|
49
|
+
this.#eventService.createSseSubscriber(req.res);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Add a subscription
|
|
53
|
+
*
|
|
54
|
+
* @example id "0d8b28c6-e9bf-4c9d-a382-3c9e0d7cfbff"
|
|
55
|
+
* @example body {"collection": "VM", "fields": ["id", "name_label"]}
|
|
56
|
+
*/
|
|
57
|
+
addSubscription(id, body) {
|
|
58
|
+
this.#eventService.addListenerFor(id, { ...body, type: body.collection });
|
|
59
|
+
return { id: body.collection };
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Remove a subscription
|
|
63
|
+
*
|
|
64
|
+
* @example id "0d8b28c6-e9bf-4c9d-a382-3c9e0d7cfbff"
|
|
65
|
+
* @example subscriptionId: "VM"
|
|
66
|
+
*/
|
|
67
|
+
removeSubscription(id, subscriptionId) {
|
|
68
|
+
this.#eventService.removeListenerFor(id, subscriptionId);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
__decorate([
|
|
72
|
+
Get(''),
|
|
73
|
+
SuccessResponse(200, 'OK'),
|
|
74
|
+
__param(0, Request())
|
|
75
|
+
], EventController.prototype, "openSseConnection", null);
|
|
76
|
+
__decorate([
|
|
77
|
+
Example(addSubscription),
|
|
78
|
+
Post('{id}/subscriptions'),
|
|
79
|
+
Middlewares(json()),
|
|
80
|
+
SuccessResponse(createdResp.status, createdResp.description),
|
|
81
|
+
Response(notFoundResp.status, notFoundResp.description),
|
|
82
|
+
__param(0, Path()),
|
|
83
|
+
__param(1, Body())
|
|
84
|
+
], EventController.prototype, "addSubscription", null);
|
|
85
|
+
__decorate([
|
|
86
|
+
Delete('{id}/subscriptions/{subscriptionId}'),
|
|
87
|
+
SuccessResponse(noContentResp.status, noContentResp.description),
|
|
88
|
+
Response(notFoundResp.status, notFoundResp.description),
|
|
89
|
+
__param(0, Path()),
|
|
90
|
+
__param(1, Path())
|
|
91
|
+
], EventController.prototype, "removeSubscription", null);
|
|
92
|
+
EventController = __decorate([
|
|
93
|
+
Route('events'),
|
|
94
|
+
Security('*'),
|
|
95
|
+
Response(badRequestResp.status, badRequestResp.description),
|
|
96
|
+
Response(unauthorizedResp.status, unauthorizedResp.description),
|
|
97
|
+
Tags('events'),
|
|
98
|
+
provide(EventController),
|
|
99
|
+
__param(0, inject(EventService))
|
|
100
|
+
], EventController);
|
|
101
|
+
export { EventController };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { createLogger } from '@xen-orchestra/log';
|
|
2
|
+
import { PingListener, Subscriber, SubscriberManager, XapiXoListener } from './event.class.mjs';
|
|
3
|
+
import { AlarmService } from '../alarms/alarm.service.mjs';
|
|
4
|
+
const log = createLogger('xo:rest-api:event-service');
|
|
5
|
+
export class EventService {
|
|
6
|
+
#alarmService;
|
|
7
|
+
#restApi;
|
|
8
|
+
#listeners = new Map();
|
|
9
|
+
#subscriberManager = new SubscriberManager();
|
|
10
|
+
constructor(restApi) {
|
|
11
|
+
process.on('SIGTERM', () => {
|
|
12
|
+
log.debug(`SIGTERM received, close all SSE subscribers (nb: ${this.#subscriberManager.subscribers.size})`);
|
|
13
|
+
this.#subscriberManager.clear();
|
|
14
|
+
this.#listeners.forEach(l => l.clear());
|
|
15
|
+
this.#listeners.clear();
|
|
16
|
+
});
|
|
17
|
+
this.#restApi = restApi;
|
|
18
|
+
this.#alarmService = restApi.ioc.get(AlarmService);
|
|
19
|
+
}
|
|
20
|
+
#getListener(type) {
|
|
21
|
+
if (!this.#listeners.has(type)) {
|
|
22
|
+
if (type === 'ping') {
|
|
23
|
+
this.#listeners.set(type, new PingListener());
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
const isAlarm = type === 'alarm';
|
|
27
|
+
// alarm is purely XO-related; it doesn't exist at the XAPI level.
|
|
28
|
+
// alarm is a message with parsed values. So, in the case of an alarm listener, it listens for message collection.
|
|
29
|
+
const ee = this.#restApi.xoApp.objects.allIndexes.type.getEventEmitterByType(isAlarm ? 'message' : type);
|
|
30
|
+
this.#listeners.set(type, new XapiXoListener(type, ee, isAlarm ? this.#alarmService : undefined));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return this.#listeners.get(type);
|
|
34
|
+
}
|
|
35
|
+
createSseSubscriber(res) {
|
|
36
|
+
const subscriber = new Subscriber(res, this.#subscriberManager);
|
|
37
|
+
subscriber.broadcast('init', { id: subscriber.id });
|
|
38
|
+
this.addListenerFor(subscriber.id, { type: 'ping' });
|
|
39
|
+
log.debug(`new SSE subscriber added: ${subscriber.id}`);
|
|
40
|
+
log.debug(`nb subscriber: ${this.#subscriberManager.subscribers.size}`);
|
|
41
|
+
return subscriber.id;
|
|
42
|
+
}
|
|
43
|
+
addListenerFor(id, { fields, type }) {
|
|
44
|
+
const subscriber = this.#subscriberManager.getSubscriber(id);
|
|
45
|
+
const listener = this.#getListener(type);
|
|
46
|
+
listener.addSubscriber(subscriber, fields);
|
|
47
|
+
}
|
|
48
|
+
removeListenerFor(id, type) {
|
|
49
|
+
const subscriber = this.#subscriberManager.getSubscriber(id);
|
|
50
|
+
const listener = this.#getListener(type);
|
|
51
|
+
listener.removeSubscriber(subscriber.id);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/ioc/ioc.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { VdiService } from '../vdis/vdi.service.mjs';
|
|
|
11
11
|
import { UserService } from '../users/user.service.mjs';
|
|
12
12
|
import { BackupJobService } from '../backup-jobs/backup-job.service.mjs';
|
|
13
13
|
import { BackupLogService } from '../backup-logs/backup-log.service.mjs';
|
|
14
|
+
import { EventService } from '../events/event.service.mjs';
|
|
14
15
|
const iocContainer = new Container();
|
|
15
16
|
decorate(injectable(), Controller);
|
|
16
17
|
iocContainer.load(buildProviderModule());
|
|
@@ -84,5 +85,12 @@ export function setupContainer(xoApp) {
|
|
|
84
85
|
return new BackupLogService();
|
|
85
86
|
})
|
|
86
87
|
.inSingletonScope();
|
|
88
|
+
iocContainer
|
|
89
|
+
.bind(EventService)
|
|
90
|
+
.toDynamicValue(ctx => {
|
|
91
|
+
const restApi = ctx.container.get(RestApi);
|
|
92
|
+
return new EventService(restApi);
|
|
93
|
+
})
|
|
94
|
+
.inSingletonScope();
|
|
87
95
|
}
|
|
88
96
|
export { iocContainer };
|
|
@@ -51,8 +51,9 @@ export const backupArchive = {
|
|
|
51
51
|
timestamp: 1758202182963,
|
|
52
52
|
vm: {
|
|
53
53
|
uuid: '7cf6150f-a978-09e6-6b41-0d1d41967bdc',
|
|
54
|
-
name_description: '
|
|
54
|
+
name_description: 'test vm used for demo',
|
|
55
55
|
name_label: 'mra_vtp_test',
|
|
56
|
+
tags: ['tag_1'],
|
|
56
57
|
},
|
|
57
58
|
differencingVhds: 1,
|
|
58
59
|
dynamicVhds: 0,
|
|
@@ -257,3 +257,77 @@ export const vmVdis = [
|
|
|
257
257
|
href: '/rest/v0/vdis/0eb73d40-e5f8-443d-b611-a52e03858a6a',
|
|
258
258
|
},
|
|
259
259
|
];
|
|
260
|
+
export const vmDashboard = {
|
|
261
|
+
quickInfo: {
|
|
262
|
+
id: '613f541c-4bed-fc77-7ca8-2db6b68f079c',
|
|
263
|
+
power_state: 'Halted',
|
|
264
|
+
uuid: '613f541c-4bed-fc77-7ca8-2db6b68f079c',
|
|
265
|
+
name_description: 'some-random-description',
|
|
266
|
+
CPUs: {
|
|
267
|
+
number: 1,
|
|
268
|
+
},
|
|
269
|
+
mainIpAddress: '10.1.6.166',
|
|
270
|
+
os_version: {
|
|
271
|
+
name: 'Alpine Linux v3.21',
|
|
272
|
+
},
|
|
273
|
+
memory: {
|
|
274
|
+
size: 536870912,
|
|
275
|
+
},
|
|
276
|
+
creation: {
|
|
277
|
+
date: '2025-10-23T14:12:05.689Z',
|
|
278
|
+
user: 'e531b8c9-3876-4ed9-8fd2-0476d5f825c9',
|
|
279
|
+
},
|
|
280
|
+
$pool: 'b7569d99-30f8-178a-7d94-801de3e29b5b',
|
|
281
|
+
virtualizationMode: 'hvm',
|
|
282
|
+
tags: [],
|
|
283
|
+
host: 'b61a5c92-700e-4966-a13b-00633f03eea8',
|
|
284
|
+
pvDriversDetected: false,
|
|
285
|
+
startTime: null,
|
|
286
|
+
},
|
|
287
|
+
alarms: [],
|
|
288
|
+
backupsInfo: {
|
|
289
|
+
lastRun: [
|
|
290
|
+
{
|
|
291
|
+
backupJobId: '399f368a-a550-4cdf-9c5b-84b68912b748',
|
|
292
|
+
timestamp: 1762124447136,
|
|
293
|
+
status: 'success',
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
backupJobId: '399f368a-a550-4cdf-9c5b-84b68912b748',
|
|
297
|
+
timestamp: 1762038039074,
|
|
298
|
+
status: 'success',
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
backupJobId: '399f368a-a550-4cdf-9c5b-84b68912b748',
|
|
302
|
+
timestamp: 1761951645862,
|
|
303
|
+
status: 'success',
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
vmProtected: true,
|
|
307
|
+
replication: {
|
|
308
|
+
id: '8c2b7a25-70b9-4a1c-d6e0-9cce86d3171a',
|
|
309
|
+
timestamp: 1761302770000,
|
|
310
|
+
sr: '4cb0d74e-a7c1-0b7d-46e3-09382c012abb',
|
|
311
|
+
},
|
|
312
|
+
backupArchives: [
|
|
313
|
+
{
|
|
314
|
+
id: '1af95910-01b4-4e87-9c2f-d895cafe0776//xo-vm-backups/613f541c-4bed-fc77-7ca8-2db6b68f079c/20251102T230026Z.json',
|
|
315
|
+
timestamp: 1762124426346,
|
|
316
|
+
backupRepository: '1af95910-01b4-4e87-9c2f-d895cafe0776',
|
|
317
|
+
size: 0,
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
id: '1af95910-01b4-4e87-9c2f-d895cafe0776//xo-vm-backups/613f541c-4bed-fc77-7ca8-2db6b68f079c/20251101T230026Z.json',
|
|
321
|
+
timestamp: 1762038026319,
|
|
322
|
+
backupRepository: '1af95910-01b4-4e87-9c2f-d895cafe0776',
|
|
323
|
+
size: 0,
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
id: '1af95910-01b4-4e87-9c2f-d895cafe0776//xo-vm-backups/613f541c-4bed-fc77-7ca8-2db6b68f079c/20251031T230025Z.json',
|
|
327
|
+
timestamp: 1761951625256,
|
|
328
|
+
backupRepository: '1af95910-01b4-4e87-9c2f-d895cafe0776',
|
|
329
|
+
size: 0,
|
|
330
|
+
},
|
|
331
|
+
],
|
|
332
|
+
},
|
|
333
|
+
};
|