@xen-orchestra/rest-api 0.22.1 → 0.23.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,11 +9,12 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
9
9
  };
10
10
  import { Example, Get, Path, Query, Response, Request, Route, Security, Tags, Post, Middlewares, Body, SuccessResponse, Put, Delete, } from 'tsoa';
11
11
  import { inject } from 'inversify';
12
+ import { invalidParameters } from 'xo-common/api-errors.js';
12
13
  import { PassThrough } from 'node:stream';
13
14
  import { provide } from 'inversify-binding-decorators';
14
15
  import { json } from 'express';
15
16
  import { RestApi } from '../rest-api/rest-api.mjs';
16
- import { asynchronousActionResp, badRequestResp, createdResp, featureUnauthorized, internalServerErrorResp, noContentResp, notFoundResp, unauthorizedResp, } from '../open-api/common/response.common.mjs';
17
+ import { asynchronousActionResp, badRequestResp, createdResp, featureUnauthorized, internalServerErrorResp, invalidParameters as invalidParametersResp, noContentResp, notFoundResp, unauthorizedResp, } from '../open-api/common/response.common.mjs';
17
18
  import { XapiXoController } from '../abstract-classes/xapi-xo-controller.mjs';
18
19
  import { AlarmService } from '../alarms/alarm.service.mjs';
19
20
  import { genericAlarmsExample } from '../open-api/oa-examples/alarm.oa-example.mjs';
@@ -265,6 +266,36 @@ let PoolController = class PoolController extends XapiXoController {
265
266
  const tasks = await this.getTasksForObject(id, { filter, limit });
266
267
  return this.sendObjects(Object.values(tasks), req, 'tasks');
267
268
  }
269
+ /**
270
+ * Reconfigure the management interface for all hosts in the pool to use the given network.
271
+ *
272
+ * Each host in the pool will switch their management interface to a PIF on the specified network.
273
+ * The PIFs on the target network must already have IP addresses configured.
274
+ *
275
+ * @example id "355ee47d-ff4c-4924-3db2-fd86ae629676"
276
+ * @example body { "network": "c787b75c-3e0d-70fa-d0c3-cbfd382d7e33" }
277
+ */
278
+ managementReconfigure(id, body, sync) {
279
+ const poolId = id;
280
+ const action = async () => {
281
+ const pool = this.getObject(poolId);
282
+ const network = this.restApi.getObject(body.network, 'network');
283
+ if (network.$pool !== pool.id) {
284
+ throw invalidParameters(`the network ${network.uuid} does not belong to pool ${pool.uuid}`);
285
+ }
286
+ const xapiPool = this.getXapiObject(poolId);
287
+ await xapiPool.$xapi.callAsync('pool.management_reconfigure', network._xapiRef);
288
+ };
289
+ return this.createAction(action, {
290
+ sync,
291
+ statusCode: noContentResp.status,
292
+ taskProperties: {
293
+ name: 'reconfigure pool management interface',
294
+ objectId: poolId,
295
+ args: body,
296
+ },
297
+ });
298
+ }
268
299
  };
269
300
  __decorate([
270
301
  Example(poolIds),
@@ -426,6 +457,20 @@ __decorate([
426
457
  __param(4, Query()),
427
458
  __param(5, Query())
428
459
  ], PoolController.prototype, "getPoolTasks", null);
460
+ __decorate([
461
+ Example(taskLocation),
462
+ Post('{id}/actions/management_reconfigure'),
463
+ Middlewares(json()),
464
+ SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description),
465
+ Response(noContentResp.status, noContentResp.description),
466
+ Response(notFoundResp.status, notFoundResp.description),
467
+ Response(badRequestResp.status, badRequestResp.description),
468
+ Response(invalidParametersResp.status, invalidParametersResp.description),
469
+ Response(internalServerErrorResp.status, internalServerErrorResp.description),
470
+ __param(0, Path()),
471
+ __param(1, Body()),
472
+ __param(2, Query())
473
+ ], PoolController.prototype, "managementReconfigure", null);
429
474
  PoolController = __decorate([
430
475
  Route('pools'),
431
476
  Security('*'),
@@ -7,14 +7,17 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  var __param = (this && this.__param) || function (paramIndex, decorator) {
8
8
  return function (target, key) { decorator(target, key, paramIndex); }
9
9
  };
10
- import { Example, Get, Path, Query, Request, Response, Route, Security, Tags } from 'tsoa';
10
+ import { Body, Delete, Example, Get, Middlewares, Path, Post, Query, Request, Response, Route, Security, SuccessResponse, Tags, } from 'tsoa';
11
+ import { json } from 'express';
11
12
  import { inject } from 'inversify';
13
+ import { invalidParameters as invalidParametersError } from 'xo-common/api-errors.js';
12
14
  import { provide } from 'inversify-binding-decorators';
13
15
  import { AlarmService } from '../alarms/alarm.service.mjs';
14
16
  import { escapeUnsafeComplexMatcher } from '../helpers/utils.helper.mjs';
15
17
  import { genericAlarmsExample } from '../open-api/oa-examples/alarm.oa-example.mjs';
16
- import { badRequestResp, notFoundResp, unauthorizedResp } from '../open-api/common/response.common.mjs';
17
- import { partialVbds, vbd, vbdIds } from '../open-api/oa-examples/vbd.oa-example.mjs';
18
+ import { badRequestResp, createdResp, invalidParameters, noContentResp, notFoundResp, unauthorizedResp, } from '../open-api/common/response.common.mjs';
19
+ import { BASE_URL } from '../index.mjs';
20
+ import { partialVbds, vbd, vbdId, vbdIds } from '../open-api/oa-examples/vbd.oa-example.mjs';
18
21
  import { RestApi } from '../rest-api/rest-api.mjs';
19
22
  import { XapiXoController } from '../abstract-classes/xapi-xo-controller.mjs';
20
23
  import { messageIds, partialMessages } from '../open-api/oa-examples/message.oa-example.mjs';
@@ -25,6 +28,29 @@ let VbdController = class VbdController extends XapiXoController {
25
28
  super('VBD', restApi);
26
29
  this.#alarmService = alarmService;
27
30
  }
31
+ /**
32
+ * Create a VBD to attach a VDI to a VM
33
+ *
34
+ * @example body { "VM": "4fe90510-8da4-1530-38e2-a7876ef374c7", "VDI": "656052a2-2e3e-467b-88ba-63a9ea5e4a54", "bootable": false, "mode": "RW" }
35
+ */
36
+ async createVbd(body) {
37
+ const xoVm = this.restApi.getObject(body.VM, 'VM');
38
+ const xoVdi = this.restApi.getObject(body.VDI, 'VDI');
39
+ if (xoVm.$pool !== xoVdi.$pool) {
40
+ throw invalidParametersError('VM and VDI must be in the same pool');
41
+ }
42
+ const xapiVm = this.restApi.getXapiObject(xoVm.id, 'VM');
43
+ const xapiVdi = this.restApi.getXapiObject(xoVdi.id, 'VDI');
44
+ const xapi = xapiVm.$xapi;
45
+ const vbdRef = await xapi.VBD_create({
46
+ ...body,
47
+ VDI: xapiVdi.$ref,
48
+ VM: xapiVm.$ref,
49
+ });
50
+ const vbdUuid = await xapi.call('VBD.get_uuid', vbdRef);
51
+ this.setHeader('Location', `${BASE_URL}/vbds/${vbdUuid}`);
52
+ return { id: vbdUuid };
53
+ }
28
54
  /**
29
55
  *
30
56
  * @example fields "device,bootable,uuid"
@@ -41,6 +67,18 @@ let VbdController = class VbdController extends XapiXoController {
41
67
  getVbd(id) {
42
68
  return this.getObject(id);
43
69
  }
70
+ /**
71
+ * Delete a VBD
72
+ *
73
+ * Removes the virtual block device, detaching the VDI from the VM.
74
+ * The VDI itself is NOT deleted.
75
+ *
76
+ * @example id "f07ab729-c0e8-721c-45ec-f11276377030"
77
+ */
78
+ async deleteVbd(id) {
79
+ const xapiVbd = this.getXapiObject(id);
80
+ await xapiVbd.$xapi.VBD_destroy(xapiVbd.$ref);
81
+ }
44
82
  /**
45
83
  * @example id "f07ab729-c0e8-721c-45ec-f11276377030"
46
84
  * @example fields "id,time"
@@ -76,6 +114,15 @@ let VbdController = class VbdController extends XapiXoController {
76
114
  return this.sendObjects(Object.values(tasks), req, 'tasks');
77
115
  }
78
116
  };
117
+ __decorate([
118
+ Example(vbdId),
119
+ Post(''),
120
+ Middlewares(json()),
121
+ SuccessResponse(createdResp.status, createdResp.description),
122
+ Response(notFoundResp.status, notFoundResp.description),
123
+ Response(invalidParameters.status, invalidParameters.description),
124
+ __param(0, Body())
125
+ ], VbdController.prototype, "createVbd", null);
79
126
  __decorate([
80
127
  Example(vbdIds),
81
128
  Example(partialVbds),
@@ -92,6 +139,12 @@ __decorate([
92
139
  Response(notFoundResp.status, notFoundResp.description),
93
140
  __param(0, Path())
94
141
  ], VbdController.prototype, "getVbd", null);
142
+ __decorate([
143
+ Delete('{id}'),
144
+ SuccessResponse(noContentResp.status, noContentResp.description),
145
+ Response(notFoundResp.status, notFoundResp.description),
146
+ __param(0, Path())
147
+ ], VbdController.prototype, "deleteVbd", null);
95
148
  __decorate([
96
149
  Example(genericAlarmsExample),
97
150
  Get('{id}/alarms'),
@@ -7,7 +7,8 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  var __param = (this && this.__param) || function (paramIndex, decorator) {
8
8
  return function (target, key) { decorator(target, key, paramIndex); }
9
9
  };
10
- import { Example, Get, Path, Post, Query, Request, Response, Route, Security, Tags, SuccessResponse, Body, Put, Delete, } from 'tsoa';
10
+ import { Example, Get, Path, Post, Query, Request, Response, Route, Security, Tags, SuccessResponse, Body, Put, Delete, Middlewares, } from 'tsoa';
11
+ import { json } from 'express';
11
12
  import { inject } from 'inversify';
12
13
  import { incorrectState, invalidParameters } from 'xo-common/api-errors.js';
13
14
  import { provide } from 'inversify-binding-decorators';
@@ -480,6 +481,7 @@ __decorate([
480
481
  __decorate([
481
482
  Example(taskLocation),
482
483
  Post('{id}/actions/start'),
484
+ Middlewares(json()),
483
485
  SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description),
484
486
  Response(noContentResp.status, noContentResp.description),
485
487
  Response(notFoundResp.status, notFoundResp.description),
@@ -21,13 +21,17 @@ export class VmService {
21
21
  this.#backupLogService = restApi.ioc.get(BackupLogService);
22
22
  }
23
23
  async #create($defer, params) {
24
- const { pool, template, cloud_config, boot, destroy_cloud_config_vdi, network_config, ...rest } = params;
24
+ const { pool, template, cloud_config, boot, destroy_cloud_config_vdi, network_config, createVtpm, ...rest } = params;
25
25
  const xoApp = this.#restApi.xoApp;
26
26
  const xapi = xoApp.getXapi(pool);
27
27
  const currentUser = this.#restApi.getCurrentUser();
28
28
  const xapiVm = await xapi.createVm(template, rest, undefined, currentUser.id);
29
29
  $defer.onFailure(() => xapi.VM_destroy(xapiVm.$ref));
30
30
  const xoVm = this.#restApi.getObject(xapiVm.uuid, 'VM');
31
+ if (createVtpm) {
32
+ const vtpmRef = await xapi.VTPM_create({ VM: xapiVm.$ref });
33
+ $defer.onFailure(() => xapi.call('VTPM.destroy', vtpmRef));
34
+ }
31
35
  let cloudConfigVdi;
32
36
  if (cloud_config !== undefined) {
33
37
  const cloudConfigVdiUuid = await xapi.VM_createCloudInitConfig(xapiVm.$ref, cloud_config, {
@@ -88,6 +92,8 @@ export class VmService {
88
92
  }
89
93
  }
90
94
  return {
95
+ active: nRunning + nPaused,
96
+ inactive: nHalted + nSuspended,
91
97
  running: nRunning,
92
98
  halted: nHalted,
93
99
  paused: nPaused,
@@ -103,7 +109,7 @@ export class VmService {
103
109
  for (const vbdId of vm.$VBDs) {
104
110
  const vbd = getObject(vbdId, 'VBD');
105
111
  if (vbd.VDI !== undefined) {
106
- const vdi = getObject(vbd.VDI, ['VDI-snapshot', 'VDI']);
112
+ const vdi = getObject(vbd.VDI, [vmType === 'VM-snapshot' ? 'VDI-snapshot' : 'VDI']);
107
113
  vdis.push(vdi);
108
114
  }
109
115
  }
@@ -182,7 +188,7 @@ export class VmService {
182
188
  }
183
189
  }
184
190
  if (lastReplica === undefined) {
185
- return;
191
+ return {};
186
192
  }
187
193
  const vdis = this.getVmVdis(id, 'VM');
188
194
  let sr = undefined;
@@ -239,7 +245,7 @@ export class VmService {
239
245
  status,
240
246
  };
241
247
  });
242
- let vmProtection = 'not-in-job';
248
+ let vmProtection = 'not-in-active-job';
243
249
  if (!vmContainsNoBakTag(vm)) {
244
250
  const backupLogsByJob = groupBy(backupLogs, 'jobId');
245
251
  for (const backupJob of relevantJobsWithSchedule) {
@@ -272,7 +278,7 @@ export class VmService {
272
278
  const backupArchivesByVmByBr = await this.#restApi.xoApp.listVmBackupsNg(brIds, { vmId: vm.id });
273
279
  return Object.values(backupArchivesByVmByBr)
274
280
  .filter(backupArchiveByVm => backupArchiveByVm !== undefined)
275
- .flatMap(backupArchiveByVm => backupArchiveByVm[vm.id])
281
+ .flatMap(backupArchiveByVm => backupArchiveByVm[vm.id] ?? [])
276
282
  .sort((a, b) => b.timestamp - a.timestamp)
277
283
  .splice(0, 3)
278
284
  .map(ba => ({ id: ba.id, timestamp: ba.timestamp, backupRepository: ba.backupRepository, size: ba.size }));
@@ -1,7 +1,7 @@
1
1
  import groupBy from 'lodash/groupBy.js';
2
2
  import { featureUnauthorized } from 'xo-common/api-errors.js';
3
3
  import semver from 'semver';
4
- import { BACKUP_TYPE, VM_POWER_STATE, } from '@vates/types';
4
+ import { BACKUP_TYPE, } from '@vates/types';
5
5
  import { createLogger } from '@xen-orchestra/log';
6
6
  import { createPredicate } from 'value-matcher';
7
7
  import { extractIdsFromSimplePattern } from '@xen-orchestra/backups/extractIdsFromSimplePattern.mjs';
@@ -11,6 +11,8 @@ 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
13
  import { BackupLogService } from '../backup-logs/backup-log.service.mjs';
14
+ import { VmService } from '../vms/vm.service.mjs';
15
+ import { BackupJobService } from '../backup-jobs/backup-job.service.mjs';
14
16
  const log = createLogger('xo:rest-api:xoa-service');
15
17
  export class XoaService {
16
18
  #restApi;
@@ -18,6 +20,8 @@ export class XoaService {
18
20
  #dashboardAsyncCache = new Map();
19
21
  #dashboardCacheOpts;
20
22
  #backupLogService;
23
+ #vmService;
24
+ #backupJobService;
21
25
  constructor(restApi) {
22
26
  this.#restApi = restApi;
23
27
  this.#hostService = restApi.ioc.get(HostService);
@@ -26,6 +30,8 @@ export class XoaService {
26
30
  expiresIn: this.#restApi.xoApp.config.getOptionalDuration('rest-api.dashboardCacheExpiresIn'),
27
31
  };
28
32
  this.#backupLogService = this.#restApi.ioc.get(BackupLogService);
33
+ this.#vmService = this.#restApi.ioc.get(VmService);
34
+ this.#backupJobService = this.#restApi.ioc.get(BackupJobService);
29
35
  }
30
36
  async #getBackupRepositoriesSizeInfo() {
31
37
  const brResult = await getFromAsyncCache(this.#dashboardAsyncCache, 'backupRepositories', async () => {
@@ -34,7 +40,7 @@ export class XoaService {
34
40
  let otherBrSize;
35
41
  const backupRepositories = await xoApp.getAllRemotes();
36
42
  if (backupRepositories.length === 0) {
37
- return undefined;
43
+ return { isEmpty: true };
38
44
  }
39
45
  const backupRepositoriesInfo = await xoApp.getAllRemotesInfo();
40
46
  for (const backupRepository of backupRepositories) {
@@ -80,6 +86,10 @@ export class XoaService {
80
86
  }
81
87
  }
82
88
  }
89
+ // if only disabled BR
90
+ if (otherBrSize === undefined && s3Brsize === undefined) {
91
+ return { isEmpty: true };
92
+ }
83
93
  const result = {};
84
94
  if (s3Brsize !== undefined) {
85
95
  result.s3 = s3Brsize;
@@ -107,6 +117,9 @@ export class XoaService {
107
117
  const srs = Object.values(this.#restApi.getObjectsByType('SR', {
108
118
  filter: isSrWritableOrIso,
109
119
  }));
120
+ if (pools.length === 0 && hosts.length === 0 && srs.length === 0) {
121
+ return { isEmpty: true };
122
+ }
110
123
  const maxLenght = Math.max(hosts.length, srs.length);
111
124
  const resourcesOverview = { nCpus: 0, memorySize: 0, srSize: 0 };
112
125
  for (let index = 0; index < maxLenght; index++) {
@@ -131,6 +144,7 @@ export class XoaService {
131
144
  let nConnectedServers = 0;
132
145
  let nUnreachableServers = 0;
133
146
  let nUnknownServers = 0;
147
+ let nDisconnectedServers = 0;
134
148
  servers.forEach(server => {
135
149
  // it may happen that some servers are marked as "connected", but no pool matches "server.pool"
136
150
  // so they are counted as `nUnknownServers`
@@ -145,20 +159,23 @@ export class XoaService {
145
159
  return;
146
160
  }
147
161
  if (server.status === 'disconnected') {
162
+ nDisconnectedServers++;
148
163
  return;
149
164
  }
150
165
  nUnknownServers++;
151
166
  });
152
167
  return {
153
168
  connected: nConnectedServers,
169
+ disconnected: nDisconnectedServers,
154
170
  unreachable: nUnreachableServers,
155
171
  unknown: nUnknownServers,
172
+ total: servers.length,
156
173
  };
157
174
  }
158
175
  async #getNumberOfEolHosts() {
159
176
  const getHVSupportedVersions = this.#restApi.xoApp.getHVSupportedVersions;
160
177
  if (getHVSupportedVersions === undefined) {
161
- return;
178
+ return { isEmpty: true };
162
179
  }
163
180
  const hvSupportedVersions = await getHVSupportedVersions();
164
181
  const hosts = this.#restApi.getObjectsByType('host');
@@ -176,11 +193,15 @@ export class XoaService {
176
193
  */
177
194
  async #getMissingPatchesInfo() {
178
195
  const missingPatchesInfo = await this.#hostService.getMissingPatchesInfo();
196
+ const eolHosts = await this.#getNumberOfEolHosts();
179
197
  const { hasAuthorization, nHostsFailed, nHostsWithMissingPatches, nPoolsWithMissingPatches } = missingPatchesInfo;
180
198
  return {
181
199
  hasAuthorization,
200
+ nHosts: this.#getNumberOfHosts(),
182
201
  nHostsFailed,
183
202
  nHostsWithMissingPatches,
203
+ nHostsEol: eolHosts,
204
+ nPools: this.#getNumberOfPools(),
184
205
  nPoolsWithMissingPatches,
185
206
  };
186
207
  }
@@ -224,11 +245,14 @@ export class XoaService {
224
245
  const writableSrs = this.#restApi.getObjectsByType('SR', {
225
246
  filter: isSrWritableOrIso,
226
247
  });
248
+ const srs = Object.values(writableSrs);
249
+ if (srs.length === 0) {
250
+ return { isEmpty: true };
251
+ }
227
252
  let replicated = 0;
228
253
  let total = 0;
229
254
  let used = 0;
230
- for (const srId in writableSrs) {
231
- const sr = writableSrs[srId];
255
+ for (const sr of srs) {
232
256
  const cache = new Set();
233
257
  const { VDIs } = sr;
234
258
  replicated += VDIs.reduce((total, vdi) => total + this.#calculateReplicatedSize(vdi, cache), 0);
@@ -239,7 +263,7 @@ export class XoaService {
239
263
  size: { available: total - used, other: used - replicated, replicated, total, used },
240
264
  };
241
265
  }
242
- async #getbackupsInfo() {
266
+ async #getBackupsInfo() {
243
267
  const vmIdsProtected = new Set();
244
268
  const vmIdsUnprotected = new Set();
245
269
  const nonReplicaVms = Object.values(this.#restApi.getObjectsByType('VM', { filter: vm => !isReplicaVm(vm) }));
@@ -281,26 +305,6 @@ export class XoaService {
281
305
  vmIdsUnprotected.add(vmId);
282
306
  }
283
307
  }
284
- async function _jobHasAtLeastOneScheduleEnabled(job) {
285
- for (const maybeScheduleId in job.settings) {
286
- if (maybeScheduleId === '') {
287
- continue;
288
- }
289
- try {
290
- const schedule = await xoApp.getSchedule(maybeScheduleId);
291
- if (schedule.enabled) {
292
- return true;
293
- }
294
- }
295
- catch (error) {
296
- if (!noSuchObject.is(error, { id: maybeScheduleId, type: 'schedule' })) {
297
- console.error(error);
298
- }
299
- continue;
300
- }
301
- }
302
- return false;
303
- }
304
308
  const backupsResult = await getFromAsyncCache(this.#dashboardAsyncCache, 'backups', async () => {
305
309
  const [logs, jobs] = await Promise.all([
306
310
  xoApp.getBackupNgLogsSorted({
@@ -312,58 +316,106 @@ export class XoaService {
312
316
  xoApp.getAllJobs('metadataBackup'),
313
317
  ]).then(jobs => jobs.flat(1)),
314
318
  ]);
319
+ if (jobs.length === 0) {
320
+ return { isEmpty: true };
321
+ }
315
322
  const logsByJob = groupBy(logs, 'jobId');
316
323
  let disabledJobs = 0;
317
324
  let failedJobs = 0;
318
325
  let skippedJobs = 0;
319
326
  let successfulJobs = 0;
327
+ let noRecentRun = 0;
320
328
  const backupJobIssues = [];
329
+ const now = new Date();
330
+ const sevenDaysAgo = new Date(now);
331
+ sevenDaysAgo.setDate(now.getDate() - 7);
332
+ sevenDaysAgo.setHours(0, 0, 0, 0);
321
333
  for (const job of jobs) {
322
- if (!(await _jobHasAtLeastOneScheduleEnabled(job))) {
334
+ if (!(await this.#backupJobService.backupJobHasAtLeastOneScheduleEnabled(job.id))) {
323
335
  _processVmsProtection(job, false);
324
336
  disabledJobs++;
325
337
  continue;
326
338
  }
327
- // Get only the last 3 runs
328
- const jobLogs = logsByJob[job.id]?.slice(-3).reverse();
329
- if (jobLogs === undefined || jobLogs.length === 0) {
339
+ const last3BackupLogs = [];
340
+ const backupLogsOfTheWeek = [];
341
+ logsByJob[job.id]?.reverse().forEach(log => {
342
+ if (log.status === 'pending') {
343
+ return;
344
+ }
345
+ const nonPendingLog = log;
346
+ if (last3BackupLogs.length < 3) {
347
+ last3BackupLogs.push(nonPendingLog);
348
+ }
349
+ if (log.start > sevenDaysAgo.getTime()) {
350
+ backupLogsOfTheWeek.push(nonPendingLog);
351
+ }
352
+ });
353
+ if (backupLogsOfTheWeek.length === 0) {
354
+ noRecentRun++;
355
+ }
356
+ else {
357
+ let hasSoftFailure = false;
358
+ let hasHardFailure = false;
359
+ for (const log of backupLogsOfTheWeek) {
360
+ if (log.status === 'interrupted' || log.status === 'failure') {
361
+ hasHardFailure = true;
362
+ break;
363
+ }
364
+ if (log.status === 'skipped') {
365
+ hasSoftFailure = true;
366
+ }
367
+ }
368
+ if (hasHardFailure) {
369
+ failedJobs++;
370
+ }
371
+ else if (hasSoftFailure) {
372
+ skippedJobs++;
373
+ }
374
+ else {
375
+ successfulJobs++;
376
+ }
377
+ }
378
+ if (last3BackupLogs.length === 0) {
330
379
  _processVmsProtection(job, false);
331
380
  continue;
332
381
  }
333
382
  if (job.type === BACKUP_TYPE.backup) {
334
- const lastJobLog = jobLogs[0];
335
- const { tasks, status } = lastJobLog;
336
- if (tasks === undefined) {
337
- _processVmsProtection(job, status === 'success');
383
+ // VM should only be considered protected if these last logs have been successful
384
+ const backupLogStatusesByVm = {};
385
+ function updateVmBackupLogStatuses(id, isSuccess) {
386
+ if (backupLogStatusesByVm[id] === undefined) {
387
+ backupLogStatusesByVm[id] = [];
388
+ }
389
+ backupLogStatusesByVm[id].push(isSuccess);
338
390
  }
339
- else {
340
- // @TODO: remove as when logs are correctly typed
341
- ;
342
- tasks.forEach(task => {
343
- _updateVmProtection(task.data.id, task.status === 'success');
344
- });
391
+ last3BackupLogs.forEach(jobLob => {
392
+ const { tasks, status } = jobLob;
393
+ if (tasks === undefined) {
394
+ const vmIds = _extractVmIdsFromBackupJob(job);
395
+ vmIds.forEach(vmId => updateVmBackupLogStatuses(vmId, status === 'success'));
396
+ }
397
+ else {
398
+ tasks.forEach(task => {
399
+ if (task.data === undefined) {
400
+ return;
401
+ }
402
+ updateVmBackupLogStatuses(task.data.id, task.status === 'success');
403
+ });
404
+ }
405
+ });
406
+ for (const [vmId, statuses] of Object.entries(backupLogStatusesByVm)) {
407
+ _updateVmProtection(vmId, statuses.every(status => status));
345
408
  }
346
409
  }
347
- const failedLog = jobLogs.find(log => log.status !== 'success');
410
+ const failedLog = last3BackupLogs.find(log => log.status !== 'success');
348
411
  if (failedLog !== undefined) {
349
- const { status } = failedLog;
350
- if (status === 'failure' || status === 'interrupted') {
351
- failedJobs++;
352
- }
353
- else if (status === 'skipped') {
354
- skippedJobs++;
355
- }
356
412
  backupJobIssues.push({
357
- // @TODO: remove as when logs are correctly typed
358
- logs: jobLogs.map(log => log.status),
413
+ logs: last3BackupLogs.map(log => log.status),
359
414
  name: job.name,
360
415
  type: job.type,
361
416
  uuid: job.id,
362
417
  });
363
418
  }
364
- else {
365
- successfulJobs++;
366
- }
367
419
  }
368
420
  const nVmsProtected = vmIdsProtected.size;
369
421
  const nVmsUnprotected = vmIdsUnprotected.size;
@@ -372,6 +424,7 @@ export class XoaService {
372
424
  jobs: {
373
425
  disabled: disabledJobs,
374
426
  failed: failedJobs,
427
+ noRecentRun,
375
428
  skipped: skippedJobs,
376
429
  successful: successfulJobs,
377
430
  total: jobs.length,
@@ -389,8 +442,9 @@ export class XoaService {
389
442
  }
390
443
  }
391
444
  #getHostsStatus() {
392
- const { running, halted, total, unknown } = this.#hostService.getHostsStatus();
445
+ const { disabled, running, halted, total, unknown } = this.#hostService.getHostsStatus();
393
446
  return {
447
+ disabled,
394
448
  running,
395
449
  halted,
396
450
  unknown,
@@ -398,37 +452,10 @@ export class XoaService {
398
452
  };
399
453
  }
400
454
  #getVmsStatus() {
401
- const vms = this.#restApi.getObjectsByType('VM');
402
- let nActive = 0;
403
- let nInactive = 0;
404
- let nUnknown = 0;
405
- let total = 0;
406
- for (const id in vms) {
407
- total++;
408
- const vm = vms[id];
409
- switch (vm.power_state) {
410
- case VM_POWER_STATE.RUNNING:
411
- case VM_POWER_STATE.PAUSED:
412
- nActive++;
413
- break;
414
- case VM_POWER_STATE.HALTED:
415
- case VM_POWER_STATE.SUSPENDED:
416
- nInactive++;
417
- break;
418
- default:
419
- nUnknown++;
420
- break;
421
- }
422
- }
423
- return {
424
- active: nActive,
425
- inactive: nInactive,
426
- unknown: nUnknown,
427
- total,
428
- };
455
+ return this.#vmService.getVmsStatus();
429
456
  }
430
457
  async getDashboard({ stream } = {}) {
431
- const [nPools, nHosts, hostsStatus, resourcesOverview, vmsStatus, storageRepositories, poolsStatus, missingPatches, backupRepositories, nHostsEol, backups,] = await Promise.all([
458
+ const [nPools, nHosts, hostsStatus, resourcesOverview, vmsStatus, storageRepositories, poolsStatus, missingPatches, backupRepositories, backups,] = await Promise.all([
432
459
  promiseWriteInStream({ maybePromise: this.#getNumberOfPools(), path: 'nPools', stream }),
433
460
  promiseWriteInStream({ maybePromise: this.#getNumberOfHosts(), path: 'nHosts', stream }),
434
461
  promiseWriteInStream({ maybePromise: this.#getHostsStatus(), path: 'hostsStatus', stream }),
@@ -450,6 +477,7 @@ export class XoaService {
450
477
  }),
451
478
  path: 'missingPatches',
452
479
  stream,
480
+ handleError: true,
453
481
  }),
454
482
  promiseWriteInStream({
455
483
  maybePromise: this.#getBackupRepositoriesSizeInfo(),
@@ -458,13 +486,7 @@ export class XoaService {
458
486
  handleError: true,
459
487
  }),
460
488
  promiseWriteInStream({
461
- maybePromise: this.#getNumberOfEolHosts(),
462
- path: 'nHostsEol',
463
- stream,
464
- handleError: true,
465
- }),
466
- promiseWriteInStream({
467
- maybePromise: this.#getbackupsInfo(),
489
+ maybePromise: this.#getBackupsInfo(),
468
490
  path: 'backups',
469
491
  stream,
470
492
  handleError: true,
@@ -476,7 +498,6 @@ export class XoaService {
476
498
  backupRepositories,
477
499
  resourcesOverview,
478
500
  poolsStatus,
479
- nHostsEol,
480
501
  missingPatches,
481
502
  storageRepositories,
482
503
  backups,