@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.
- package/dist/hosts/host.controller.mjs +48 -3
- package/dist/open-api/oa-examples/vbd.oa-example.mjs +3 -0
- package/dist/open-api/oa-examples/xoa.oa-example.mjs +30 -40
- package/dist/open-api/routes/routes.js +173 -21
- package/dist/pools/pool.controller.mjs +46 -1
- package/dist/vbds/vbd.controller.mjs +56 -3
- package/dist/vms/vm.controller.mjs +3 -1
- package/dist/vms/vm.service.mjs +11 -5
- package/dist/xoa/xoa.service.mjs +112 -91
- package/open-api/spec/swagger.json +849 -554
- package/package.json +4 -4
|
@@ -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 {
|
|
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),
|
package/dist/vms/vm.service.mjs
CHANGED
|
@@ -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'
|
|
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 }));
|
package/dist/xoa/xoa.service.mjs
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
|
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 #
|
|
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
|
|
334
|
+
if (!(await this.#backupJobService.backupJobHasAtLeastOneScheduleEnabled(job.id))) {
|
|
323
335
|
_processVmsProtection(job, false);
|
|
324
336
|
disabledJobs++;
|
|
325
337
|
continue;
|
|
326
338
|
}
|
|
327
|
-
|
|
328
|
-
const
|
|
329
|
-
|
|
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
|
-
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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.#
|
|
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,
|