@xen-orchestra/rest-api 0.19.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.
Files changed (30) hide show
  1. package/dist/abstract-classes/base-controller.mjs +2 -2
  2. package/dist/abstract-classes/listener.mjs +59 -0
  3. package/dist/backup-jobs/backup-job.controller.mjs +18 -8
  4. package/dist/backup-jobs/backup-job.service.mjs +28 -2
  5. package/dist/backup-logs/backup-log.service.mjs +8 -1
  6. package/dist/events/event.class.mjs +127 -0
  7. package/dist/events/event.controller.mjs +101 -0
  8. package/dist/events/event.service.mjs +53 -0
  9. package/dist/events/event.type.mjs +1 -0
  10. package/dist/ioc/ioc.mjs +8 -0
  11. package/dist/middlewares/authentication.middleware.mjs +3 -0
  12. package/dist/open-api/common/response.common.mjs +0 -1
  13. package/dist/open-api/oa-examples/backup-archive.oa-example.mjs +2 -1
  14. package/dist/open-api/oa-examples/event.oa-example.mjs +3 -0
  15. package/dist/open-api/oa-examples/gui-routes.oa-example.mjs +4 -0
  16. package/dist/open-api/oa-examples/ping.oa-example.mjs +4 -0
  17. package/dist/open-api/oa-examples/task.oa-example.mjs +1 -1
  18. package/dist/open-api/oa-examples/vm.oa-example.mjs +74 -0
  19. package/dist/open-api/routes/routes.js +256 -22
  20. package/dist/pools/pool.controller.mjs +5 -5
  21. package/dist/schedules/schedule.controller.mjs +1 -1
  22. package/dist/servers/server.controller.mjs +2 -2
  23. package/dist/tasks/task.controller.mjs +1 -1
  24. package/dist/vms/vm.controller.mjs +44 -23
  25. package/dist/vms/vm.service.mjs +181 -0
  26. package/dist/vms/vm.type.mjs +1 -0
  27. package/dist/xoa/xoa.controller.mjs +23 -2
  28. package/dist/xoa/xoa.service.mjs +21 -1
  29. package/open-api/spec/swagger.json +1045 -197
  30. package/package.json +4 -3
@@ -289,7 +289,7 @@ __decorate([
289
289
  Middlewares(json()),
290
290
  Tags('networks'),
291
291
  SuccessResponse(createdResp.status, createdResp.description),
292
- Response(asynchronousActionResp.status, asynchronousActionResp.description, asynchronousActionResp.produce),
292
+ Response(asynchronousActionResp.status, asynchronousActionResp.description),
293
293
  Response(notFoundResp.status, notFoundResp.description),
294
294
  Response(internalServerErrorResp.status, internalServerErrorResp.description),
295
295
  __param(0, Path()),
@@ -299,7 +299,7 @@ __decorate([
299
299
  __decorate([
300
300
  Example(taskLocation),
301
301
  Post('{id}/actions/emergency_shutdown'),
302
- SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description, asynchronousActionResp.produce),
302
+ SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description),
303
303
  Response(noContentResp.status, noContentResp.description),
304
304
  Response(featureUnauthorized.status, featureUnauthorized.description),
305
305
  Response(notFoundResp.status, notFoundResp.description),
@@ -309,7 +309,7 @@ __decorate([
309
309
  __decorate([
310
310
  Example(taskLocation),
311
311
  Post('{id}/actions/rolling_reboot'),
312
- SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description, asynchronousActionResp.produce),
312
+ SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description),
313
313
  Response(noContentResp.status, noContentResp.description),
314
314
  Response(featureUnauthorized.status, featureUnauthorized.description),
315
315
  Response(notFoundResp.status, notFoundResp.description),
@@ -319,7 +319,7 @@ __decorate([
319
319
  __decorate([
320
320
  Example(taskLocation),
321
321
  Post('{id}/actions/rolling_update'),
322
- SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description, asynchronousActionResp.produce),
322
+ SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description),
323
323
  Response(noContentResp.status, noContentResp.description),
324
324
  Response(featureUnauthorized.status, featureUnauthorized.description),
325
325
  Response(notFoundResp.status, notFoundResp.description),
@@ -344,7 +344,7 @@ __decorate([
344
344
  Middlewares(json()),
345
345
  Tags('vms'),
346
346
  SuccessResponse(createdResp.status, createdResp.description),
347
- Response(asynchronousActionResp.status, asynchronousActionResp.description, asynchronousActionResp.produce),
347
+ Response(asynchronousActionResp.status, asynchronousActionResp.description),
348
348
  Response(notFoundResp.status, notFoundResp.description),
349
349
  Response(internalServerErrorResp.status, internalServerErrorResp.description),
350
350
  __param(0, Path()),
@@ -72,7 +72,7 @@ __decorate([
72
72
  __decorate([
73
73
  Example(taskLocation),
74
74
  Post('{id}/actions/run'),
75
- SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description, asynchronousActionResp.produce),
75
+ SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description),
76
76
  Response(noContentResp.status, noContentResp.description),
77
77
  Response(featureUnauthorized.status, featureUnauthorized.description),
78
78
  Response(notFoundResp.status, notFoundResp.description),
@@ -128,7 +128,7 @@ __decorate([
128
128
  __decorate([
129
129
  Example(taskLocation),
130
130
  Post('{id}/actions/connect'),
131
- SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description, asynchronousActionResp.produce),
131
+ SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description),
132
132
  Response(noContentResp.status, noContentResp.description),
133
133
  Response(notFoundResp.status, notFoundResp.description),
134
134
  Response(409, 'The server is already connected'),
@@ -138,7 +138,7 @@ __decorate([
138
138
  __decorate([
139
139
  Example(taskLocation),
140
140
  Post('{id}/actions/disconnect'),
141
- SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description, asynchronousActionResp.produce),
141
+ SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description),
142
142
  Response(noContentResp.status, noContentResp.description),
143
143
  Response(notFoundResp.status, notFoundResp.description),
144
144
  Response(409, 'The server is already disconnected'),
@@ -151,7 +151,7 @@ __decorate([
151
151
  __decorate([
152
152
  Example(taskLocation),
153
153
  Post('{id}/actions/abort'),
154
- SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description, asynchronousActionResp.produce),
154
+ SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description),
155
155
  Response(noContentResp.status, noContentResp.description),
156
156
  Response(notFoundResp.status, notFoundResp.description),
157
157
  __param(0, Path()),
@@ -11,12 +11,12 @@ import { Example, Get, Path, Post, Query, Request, Response, Route, Security, Ta
11
11
  import { inject } from 'inversify';
12
12
  import { incorrectState, invalidParameters } from 'xo-common/api-errors.js';
13
13
  import { provide } from 'inversify-binding-decorators';
14
- import { AlarmService } from '../alarms/alarm.service.mjs';
14
+ import { PassThrough } from 'node:stream';
15
15
  import { asynchronousActionResp, badRequestResp, createdResp, forbiddenOperationResp, incorrectStateResp, internalServerErrorResp, noContentResp, notFoundResp, unauthorizedResp, } from '../open-api/common/response.common.mjs';
16
16
  import { BASE_URL } from '../index.mjs';
17
- import { escapeUnsafeComplexMatcher, limitAndFilterArray } from '../helpers/utils.helper.mjs';
17
+ import { limitAndFilterArray, NDJSON_CONTENT_TYPE } from '../helpers/utils.helper.mjs';
18
18
  import { genericAlarmsExample } from '../open-api/oa-examples/alarm.oa-example.mjs';
19
- import { partialVms, vm, vmIds, vmStatsExample, vmVdis } from '../open-api/oa-examples/vm.oa-example.mjs';
19
+ import { partialVms, vm, vmDashboard, vmIds, vmStatsExample, vmVdis } from '../open-api/oa-examples/vm.oa-example.mjs';
20
20
  import { RestApi } from '../rest-api/rest-api.mjs';
21
21
  import { partialTasks, taskIds, taskLocation } from '../open-api/oa-examples/task.oa-example.mjs';
22
22
  import { XapiXoController } from '../abstract-classes/xapi-xo-controller.mjs';
@@ -26,12 +26,10 @@ import { partialVmBackupJobs, vmBackupJobIds } from '../open-api/oa-examples/bac
26
26
  import { messageIds, partialMessages } from '../open-api/oa-examples/message.oa-example.mjs';
27
27
  const IGNORED_VDIS_TAG = '[NOSNAP]';
28
28
  let VmController = class VmController extends XapiXoController {
29
- #alarmService;
30
29
  #vmService;
31
30
  #backupJobService;
32
- constructor(restApi, alarmService, vmService, backupJobService) {
31
+ constructor(restApi, vmService, backupJobService) {
33
32
  super('VM', restApi);
34
- this.#alarmService = alarmService;
35
33
  this.#vmService = vmService;
36
34
  this.#backupJobService = backupJobService;
37
35
  }
@@ -333,11 +331,7 @@ let VmController = class VmController extends XapiXoController {
333
331
  * @example limit 42
334
332
  */
335
333
  getVmAlarms(req, id, fields, ndjson, filter, limit) {
336
- const vm = this.getObject(id);
337
- const alarms = this.#alarmService.getAlarms({
338
- filter: `${escapeUnsafeComplexMatcher(filter) ?? ''} object:uuid:${vm.uuid}`,
339
- limit,
340
- });
334
+ const alarms = this.#vmService.getVmAlarms(id, { filter, limit });
341
335
  return this.sendObjects(Object.values(alarms), req, 'alarms');
342
336
  }
343
337
  /**
@@ -402,6 +396,27 @@ let VmController = class VmController extends XapiXoController {
402
396
  const vm = this.getXapiObject(id);
403
397
  await vm.$call('remove_tags', tag);
404
398
  }
399
+ /**
400
+ * @example id "613f541c-4bed-fc77-7ca8-2db6b68f079c"
401
+ */
402
+ async getVmDashboard(req, id, ndjson) {
403
+ const stream = ndjson ? new PassThrough() : undefined;
404
+ const isStream = stream !== undefined;
405
+ if (isStream) {
406
+ const res = req.res;
407
+ res.setHeader('Content-Type', NDJSON_CONTENT_TYPE);
408
+ stream.pipe(res);
409
+ }
410
+ try {
411
+ const dashboard = await this.#vmService.getVmDashboard(id, { stream });
412
+ if (!isStream) {
413
+ return dashboard;
414
+ }
415
+ }
416
+ finally {
417
+ stream?.end();
418
+ }
419
+ }
405
420
  };
406
421
  __decorate([
407
422
  Example(vmIds),
@@ -464,7 +479,7 @@ __decorate([
464
479
  __decorate([
465
480
  Example(taskLocation),
466
481
  Post('{id}/actions/start'),
467
- SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description, asynchronousActionResp.produce),
482
+ SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description),
468
483
  Response(noContentResp.status, noContentResp.description),
469
484
  Response(notFoundResp.status, notFoundResp.description),
470
485
  Response(internalServerErrorResp.status, internalServerErrorResp.description),
@@ -475,7 +490,7 @@ __decorate([
475
490
  __decorate([
476
491
  Example(taskLocation),
477
492
  Post('{id}/actions/clean_shutdown'),
478
- SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description, asynchronousActionResp.produce),
493
+ SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description),
479
494
  Response(noContentResp.status, noContentResp.description),
480
495
  Response(notFoundResp.status, notFoundResp.description),
481
496
  Response(internalServerErrorResp.status, internalServerErrorResp.description),
@@ -491,7 +506,7 @@ __decorate([
491
506
  __decorate([
492
507
  Example(taskLocation),
493
508
  Post('{id}/actions/hard_shutdown'),
494
- SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description, asynchronousActionResp.produce),
509
+ SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description),
495
510
  Response(noContentResp.status, noContentResp.description),
496
511
  Response(notFoundResp.status, notFoundResp.description),
497
512
  Response(internalServerErrorResp.status, internalServerErrorResp.description),
@@ -501,7 +516,7 @@ __decorate([
501
516
  __decorate([
502
517
  Example(taskLocation),
503
518
  Post('{id}/actions/hard_reboot'),
504
- SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description, asynchronousActionResp.produce),
519
+ SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description),
505
520
  Response(noContentResp.status, noContentResp.description),
506
521
  Response(notFoundResp.status, notFoundResp.description),
507
522
  Response(internalServerErrorResp.status, internalServerErrorResp.description),
@@ -511,7 +526,7 @@ __decorate([
511
526
  __decorate([
512
527
  Example(taskLocation),
513
528
  Post('{id}/actions/pause'),
514
- SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description, asynchronousActionResp.produce),
529
+ SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description),
515
530
  Response(noContentResp.status, noContentResp.description),
516
531
  Response(notFoundResp.status, notFoundResp.description),
517
532
  Response(internalServerErrorResp.status, internalServerErrorResp.description),
@@ -521,7 +536,7 @@ __decorate([
521
536
  __decorate([
522
537
  Example(taskLocation),
523
538
  Post('{id}/actions/suspend'),
524
- SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description, asynchronousActionResp.produce),
539
+ SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description),
525
540
  Response(noContentResp.status, noContentResp.description),
526
541
  Response(notFoundResp.status, notFoundResp.description),
527
542
  Response(internalServerErrorResp.status, internalServerErrorResp.description),
@@ -531,7 +546,7 @@ __decorate([
531
546
  __decorate([
532
547
  Example(taskLocation),
533
548
  Post('{id}/actions/resume'),
534
- SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description, asynchronousActionResp.produce),
549
+ SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description),
535
550
  Response(noContentResp.status, noContentResp.description),
536
551
  Response(notFoundResp.status, notFoundResp.description),
537
552
  Response(internalServerErrorResp.status, internalServerErrorResp.description),
@@ -541,7 +556,7 @@ __decorate([
541
556
  __decorate([
542
557
  Example(taskLocation),
543
558
  Post('{id}/actions/unpause'),
544
- SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description, asynchronousActionResp.produce),
559
+ SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description),
545
560
  Response(noContentResp.status, noContentResp.description),
546
561
  Response(notFoundResp.status, notFoundResp.description),
547
562
  Response(internalServerErrorResp.status, internalServerErrorResp.description),
@@ -551,7 +566,7 @@ __decorate([
551
566
  __decorate([
552
567
  Example(taskLocation),
553
568
  Post('{id}/actions/snapshot'),
554
- SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description, asynchronousActionResp.produce),
569
+ SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description),
555
570
  Response(createdResp.status, 'Snapshot created'),
556
571
  Response(notFoundResp.status, notFoundResp.description),
557
572
  Response(internalServerErrorResp.status, internalServerErrorResp.description),
@@ -636,6 +651,13 @@ __decorate([
636
651
  __param(0, Path()),
637
652
  __param(1, Path())
638
653
  ], VmController.prototype, "deleteVmTag", null);
654
+ __decorate([
655
+ Example(vmDashboard),
656
+ Get('{id}/dashboard'),
657
+ __param(0, Request()),
658
+ __param(1, Path()),
659
+ __param(2, Query())
660
+ ], VmController.prototype, "getVmDashboard", null);
639
661
  VmController = __decorate([
640
662
  Route('vms'),
641
663
  Security('*'),
@@ -647,8 +669,7 @@ VmController = __decorate([
647
669
  ,
648
670
  provide(VmController),
649
671
  __param(0, inject(RestApi)),
650
- __param(1, inject(AlarmService)),
651
- __param(2, inject(VmService)),
652
- __param(3, inject(BackupJobService))
672
+ __param(1, inject(VmService)),
673
+ __param(2, inject(BackupJobService))
653
674
  ], VmController);
654
675
  export { VmController };
@@ -2,11 +2,23 @@ import { createLogger } from '@xen-orchestra/log';
2
2
  import { defer } from 'golike-defer';
3
3
  import { Task } from '@vates/task';
4
4
  import { VM_POWER_STATE, } from '@vates/types';
5
+ import { escapeUnsafeComplexMatcher, promiseWriteInStream, vmContainsNoBakTag } from '../helpers/utils.helper.mjs';
6
+ import { AlarmService } from '../alarms/alarm.service.mjs';
7
+ import { parseDateTime } from '@xen-orchestra/xapi';
8
+ import { BackupJobService } from '../backup-jobs/backup-job.service.mjs';
9
+ import groupBy from 'lodash/groupBy.js';
10
+ import { BackupLogService } from '../backup-logs/backup-log.service.mjs';
5
11
  const log = createLogger('xo:rest-api:vm-service');
6
12
  export class VmService {
7
13
  #restApi;
14
+ #alarmService;
15
+ #backupJobService;
16
+ #backupLogService;
8
17
  constructor(restApi) {
9
18
  this.#restApi = restApi;
19
+ this.#alarmService = restApi.ioc.get(AlarmService);
20
+ this.#backupJobService = restApi.ioc.get(BackupJobService);
21
+ this.#backupLogService = restApi.ioc.get(BackupLogService);
10
22
  }
11
23
  async #create($defer, params) {
12
24
  const { pool, template, cloud_config, boot, destroy_cloud_config_vdi, network_config, ...rest } = params;
@@ -115,4 +127,173 @@ export class VmService {
115
127
  }
116
128
  return stream;
117
129
  }
130
+ getVmAlarms(id, { filter, limit } = {}) {
131
+ const vm = this.#restApi.getObject(id, 'VM');
132
+ const alarms = this.#alarmService.getAlarms({
133
+ filter: `${escapeUnsafeComplexMatcher(filter) ?? ''} object:uuid:${vm.id}`,
134
+ limit,
135
+ });
136
+ return alarms;
137
+ }
138
+ #getDashboardQuickInfo(id) {
139
+ const { power_state, uuid, name_description, CPUs, mainIpAddress, os_version, memory, creation, $pool, virtualizationMode, tags, $container, startTime, pvDriversDetected, pvDriversVersion, pvDriversUpToDate, } = this.#restApi.getObject(id, 'VM');
140
+ return {
141
+ id,
142
+ power_state,
143
+ uuid,
144
+ name_description,
145
+ CPUs: {
146
+ number: CPUs.number,
147
+ },
148
+ mainIpAddress,
149
+ os_version: {
150
+ name: os_version?.name,
151
+ },
152
+ memory: {
153
+ size: memory.size,
154
+ },
155
+ creation: {
156
+ date: creation.date,
157
+ user: creation.user,
158
+ },
159
+ $pool,
160
+ virtualizationMode,
161
+ tags,
162
+ host: $container === $pool ? undefined : $container,
163
+ pvDriversDetected: pvDriversDetected ?? false,
164
+ pvDriversVersion,
165
+ pvDriversUpToDate,
166
+ startTime,
167
+ };
168
+ }
169
+ #getLastReplication(id) {
170
+ const vm = this.#restApi.getObject(id, 'VM');
171
+ const replicatedVms = this.#restApi.getObjectsByType('VM', {
172
+ filter: obj => obj.other['xo:backup:vm'] === vm.id,
173
+ });
174
+ let lastTimestamp;
175
+ let lastReplica;
176
+ for (const id in replicatedVms) {
177
+ const replica = replicatedVms[id];
178
+ const timestamp = parseDateTime(replica.other['xo:backup:datetime']);
179
+ if (lastTimestamp === undefined || lastTimestamp < timestamp) {
180
+ lastTimestamp = timestamp;
181
+ lastReplica = replica;
182
+ }
183
+ }
184
+ if (lastReplica === undefined) {
185
+ return;
186
+ }
187
+ const vdis = this.getVmVdis(id, 'VM');
188
+ let sr = undefined;
189
+ for (const vdi of vdis) {
190
+ if (sr === undefined) {
191
+ sr = vdi.$SR;
192
+ continue;
193
+ }
194
+ if (sr !== vdi.$SR) {
195
+ // if the VM has VDIs on multiple SRs, set the sr as undefined
196
+ sr = undefined;
197
+ break;
198
+ }
199
+ }
200
+ return {
201
+ id: lastReplica.id,
202
+ timestamp: lastTimestamp * 1000,
203
+ sr,
204
+ };
205
+ }
206
+ async #getBackupsInfo(id) {
207
+ const vm = this.#restApi.getObject(id, 'VM');
208
+ const allBackupJobs = await this.#restApi.xoApp.getAllJobs('backup');
209
+ const relevantJobIds = [];
210
+ const relevantJobsWithSchedule = [];
211
+ for (const backupJob of allBackupJobs) {
212
+ if (await this.#backupJobService.isVmInBackupJob(backupJob.id, vm.id)) {
213
+ relevantJobIds.push(backupJob.id);
214
+ if (await this.#backupJobService.backupJobHasAtLeastOneScheduleEnabled(backupJob.id)) {
215
+ relevantJobsWithSchedule.push(backupJob);
216
+ }
217
+ }
218
+ }
219
+ const backupLogs = (await this.#restApi.xoApp.getBackupNgLogsSorted({
220
+ filter: log => this.#backupLogService.isBackupLog(log) &&
221
+ relevantJobIds.includes(log.jobId) &&
222
+ this.#backupLogService.isVmInBackupLog(log, id),
223
+ }));
224
+ const lastBackupRuns = backupLogs
225
+ .slice(-3)
226
+ .reverse()
227
+ .map(log => {
228
+ let status;
229
+ if (log.status === 'success') {
230
+ status = log.status;
231
+ }
232
+ else {
233
+ const vmTaskLog = this.#backupLogService.getVmBackupTaskLog(log, id);
234
+ status = vmTaskLog.status;
235
+ }
236
+ return {
237
+ backupJobId: log.jobId,
238
+ timestamp: log.end,
239
+ status,
240
+ };
241
+ });
242
+ let isProtected = false;
243
+ if (!vmContainsNoBakTag(vm)) {
244
+ const backupLogsByJob = groupBy(backupLogs, 'jobId');
245
+ for (const backupJob of relevantJobsWithSchedule) {
246
+ if (isProtected) {
247
+ break;
248
+ }
249
+ // can be undefined if the backup did run for now
250
+ const jobLogs = backupLogsByJob[backupJob.id]
251
+ ?.filter(log => log.status !== 'pending')
252
+ .slice(-3);
253
+ if (jobLogs !== undefined) {
254
+ isProtected = jobLogs.every(log => {
255
+ if (log.status === 'success') {
256
+ return true;
257
+ }
258
+ const vmTaskLog = this.#backupLogService.getVmBackupTaskLog(log, id);
259
+ return vmTaskLog?.status === 'success';
260
+ });
261
+ }
262
+ }
263
+ }
264
+ return { lastRuns: lastBackupRuns, vmProtected: isProtected };
265
+ }
266
+ async #getLastVmBackupArchives(id) {
267
+ const vm = this.#restApi.getObject(id, 'VM');
268
+ const brIds = (await this.#restApi.xoApp.getAllRemotes()).map(br => br.id);
269
+ const backupArchivesByVmByBr = await this.#restApi.xoApp.listVmBackupsNg(brIds, { vmId: vm.id });
270
+ return Object.values(backupArchivesByVmByBr)
271
+ .flatMap(backupArchiveByVm => backupArchiveByVm[vm.id])
272
+ .sort((a, b) => b.timestamp - a.timestamp)
273
+ .splice(0, 3)
274
+ .map(ba => ({ id: ba.id, timestamp: ba.timestamp, backupRepository: ba.backupRepository, size: ba.size }));
275
+ }
276
+ async getVmDashboard(id, { stream } = {}) {
277
+ const [quickInfo, alarms, lastReplication, { lastRuns, vmProtected }, lastBackupArchives] = await Promise.all([
278
+ promiseWriteInStream({ maybePromise: this.#getDashboardQuickInfo(id), path: 'quickInfo', stream }),
279
+ promiseWriteInStream({ maybePromise: Object.keys(this.getVmAlarms(id)), path: 'alarms', stream }),
280
+ promiseWriteInStream({ maybePromise: this.#getLastReplication(id), path: 'backupsInfo.replication', stream }),
281
+ promiseWriteInStream({ maybePromise: this.#getBackupsInfo(id), path: 'backupsInfo', stream }),
282
+ promiseWriteInStream({
283
+ maybePromise: this.#getLastVmBackupArchives(id),
284
+ path: 'backupsInfo.backupArchives',
285
+ stream,
286
+ }),
287
+ ]);
288
+ return {
289
+ quickInfo,
290
+ alarms: alarms,
291
+ backupsInfo: {
292
+ lastRuns,
293
+ vmProtected,
294
+ replication: lastReplication,
295
+ backupArchives: lastBackupArchives,
296
+ },
297
+ };
298
+ }
118
299
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -11,6 +11,8 @@ import { Controller, Example, Get, Query, Request, Response, Route, Security, Ta
11
11
  import { inject } from 'inversify';
12
12
  import { PassThrough } from 'node:stream';
13
13
  import { provide } from 'inversify-binding-decorators';
14
+ import { guiRoutes } from '../open-api/oa-examples/gui-routes.oa-example.mjs';
15
+ import { pingResponse } from '../open-api/oa-examples/ping.oa-example.mjs';
14
16
  import { badRequestResp, unauthorizedResp } from '../open-api/common/response.common.mjs';
15
17
  import { xoaDashboard } from '../open-api/oa-examples/xoa.oa-example.mjs';
16
18
  import { XoaService } from './xoa.service.mjs';
@@ -37,18 +39,37 @@ let XoaController = class XoaController extends Controller {
37
39
  return dashboard;
38
40
  }
39
41
  }
42
+ ping() {
43
+ return {
44
+ result: 'pong',
45
+ timestamp: Date.now(),
46
+ };
47
+ }
48
+ getGuiRoutes() {
49
+ return this.#xoaService.getGuiRoutes();
50
+ }
40
51
  };
41
52
  __decorate([
42
53
  Example(xoaDashboard),
43
54
  Get('dashboard'),
55
+ Response(badRequestResp.status, badRequestResp.description),
56
+ Response(unauthorizedResp.status, unauthorizedResp.description),
44
57
  __param(0, Request()),
45
58
  __param(1, Query())
46
59
  ], XoaController.prototype, "getDashboard", null);
60
+ __decorate([
61
+ Security('none'),
62
+ Example(pingResponse),
63
+ Get('ping')
64
+ ], XoaController.prototype, "ping", null);
65
+ __decorate([
66
+ Security('none'),
67
+ Example(guiRoutes),
68
+ Get('gui-routes')
69
+ ], XoaController.prototype, "getGuiRoutes", null);
47
70
  XoaController = __decorate([
48
71
  Route(''),
49
72
  Security('*'),
50
- Response(badRequestResp.status, badRequestResp.description),
51
- Response(unauthorizedResp.status, unauthorizedResp.description),
52
73
  Tags('xoa'),
53
74
  provide(XoaController),
54
75
  __param(0, inject(XoaService))
@@ -10,12 +10,14 @@ import { parse } from 'xo-remote-parser';
10
10
  import { getFromAsyncCache } from '../helpers/cache.helper.mjs';
11
11
  import { isReplicaVm, isSrWritableOrIso, promiseWriteInStream, vmContainsNoBakTag } from '../helpers/utils.helper.mjs';
12
12
  import { HostService } from '../hosts/host.service.mjs';
13
+ import { BackupLogService } from '../backup-logs/backup-log.service.mjs';
13
14
  const log = createLogger('xo:rest-api:xoa-service');
14
15
  export class XoaService {
15
16
  #restApi;
16
17
  #hostService;
17
18
  #dashboardAsyncCache = new Map();
18
19
  #dashboardCacheOpts;
20
+ #backupLogService;
19
21
  constructor(restApi) {
20
22
  this.#restApi = restApi;
21
23
  this.#hostService = restApi.ioc.get(HostService);
@@ -23,6 +25,7 @@ export class XoaService {
23
25
  timeout: this.#restApi.xoApp.config.getOptionalDuration('rest-api.dashboardCacheTimeout') ?? 60000,
24
26
  expiresIn: this.#restApi.xoApp.config.getOptionalDuration('rest-api.dashboardCacheExpiresIn'),
25
27
  };
28
+ this.#backupLogService = this.#restApi.ioc.get(BackupLogService);
26
29
  }
27
30
  async #getBackupRepositoriesSizeInfo() {
28
31
  const brResult = await getFromAsyncCache(this.#dashboardAsyncCache, 'backupRepositories', async () => {
@@ -301,7 +304,7 @@ export class XoaService {
301
304
  const backupsResult = await getFromAsyncCache(this.#dashboardAsyncCache, 'backups', async () => {
302
305
  const [logs, jobs] = await Promise.all([
303
306
  xoApp.getBackupNgLogsSorted({
304
- filter: log => log.message === 'backup' || log.message === 'metadata',
307
+ filter: log => this.#backupLogService.isBackupLog(log),
305
308
  }),
306
309
  Promise.all([
307
310
  xoApp.getAllJobs('backup'),
@@ -481,4 +484,21 @@ export class XoaService {
481
484
  vmsStatus,
482
485
  };
483
486
  }
487
+ getGuiRoutes() {
488
+ const mounts = this.#restApi.xoApp.config.getOptional('http.mounts') ?? {};
489
+ let xo5Mount;
490
+ let xo6Mount;
491
+ for (const [key, value] of Object.entries(mounts)) {
492
+ if (value.includes('xo-web/dist')) {
493
+ xo5Mount = key;
494
+ }
495
+ else if (value.includes('@xen-orchestra/web/dist')) {
496
+ xo6Mount = key;
497
+ }
498
+ }
499
+ return {
500
+ xo5: xo5Mount,
501
+ xo6: xo6Mount,
502
+ };
503
+ }
484
504
  }