@xen-orchestra/rest-api 0.7.0 → 0.9.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/README.md +2 -1
- package/dist/abstract-classes/base-controller.mjs +14 -2
- package/dist/alarms/alarm.controller.mjs +3 -2
- package/dist/groups/group.controller.mjs +3 -2
- package/dist/helpers/cache.helper.mjs +52 -0
- package/dist/helpers/stream.helper.mjs +5 -0
- package/dist/helpers/utils.helper.mjs +3 -0
- package/dist/hosts/host.controller.mjs +3 -2
- package/dist/index.mjs +1 -1
- package/dist/ioc/ioc.mjs +8 -0
- package/dist/messages/message.controller.mjs +3 -2
- package/dist/middlewares/generic-error-handler.middleware.mjs +5 -1
- package/dist/networks/network.controller.mjs +18 -4
- package/dist/open-api/common/response.common.mjs +1 -1
- package/dist/open-api/oa-examples/pci.oa-example.mjs +30 -0
- package/dist/open-api/oa-examples/pgpu.oa-example.mjs +36 -0
- package/dist/open-api/oa-examples/pif.oa-example.mjs +51 -0
- package/dist/open-api/oa-examples/schedule.oa-example.mjs +3 -0
- package/dist/open-api/oa-examples/vm-controller.oa-example.mjs +1 -1
- package/dist/open-api/oa-examples/xoa.oa-example.mjs +61 -0
- package/dist/open-api/routes/routes.js +717 -23
- package/dist/pcis/pci.controller.mjs +60 -0
- package/dist/pgpus/pgpu.controller.mjs +60 -0
- package/dist/pifs/pif.controller.mjs +60 -0
- package/dist/pools/pool.controller.mjs +136 -4
- package/dist/pools/pool.type.mjs +1 -0
- package/dist/schedules/schedule.controller.mjs +5 -4
- package/dist/servers/server.controller.mjs +19 -6
- package/dist/srs/sr.controller.mjs +3 -2
- package/dist/users/user.controller.mjs +3 -2
- package/dist/vbds/vbd.controller.mjs +3 -2
- package/dist/vdi-snapshots/vdi-snapshot.controller.mjs +3 -2
- package/dist/vdis/vdi.controller.mjs +3 -2
- package/dist/vifs/vif.controller.mjs +3 -2
- package/dist/vm-controller/vm-controller.controller.mjs +3 -2
- package/dist/vm-snapshots/vm-snapshot.controller.mjs +3 -2
- package/dist/vm-templates/vm-template.controller.mjs +3 -2
- package/dist/vms/vm.controller.mjs +108 -8
- package/dist/xoa/xoa.controller.mjs +39 -0
- package/dist/xoa/xoa.service.mjs +407 -0
- package/dist/xoa/xoa.type.mjs +1 -0
- package/open-api/spec/swagger.json +5200 -2606
- package/package.json +10 -4
|
@@ -7,15 +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, Post, Query, Request, Response, Route, Security, Tags, SuccessResponse } from 'tsoa';
|
|
10
|
+
import { Example, Get, Path, Post, Query, Request, Response, Route, Security, Tags, SuccessResponse, Body, Put, Delete, } from 'tsoa';
|
|
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 {
|
|
14
|
+
import { asynchronousActionResp, createdResp, internalServerErrorResp, noContentResp, notFoundResp, unauthorizedResp, } from '../open-api/common/response.common.mjs';
|
|
15
|
+
import { BASE_URL } from '../index.mjs';
|
|
15
16
|
import { partialVms, vm, vmIds, vmStatsExample } from '../open-api/oa-examples/vm.oa-example.mjs';
|
|
16
17
|
import { RestApi } from '../rest-api/rest-api.mjs';
|
|
17
18
|
import { taskLocation } from '../open-api/oa-examples/task.oa-example.mjs';
|
|
18
19
|
import { XapiXoController } from '../abstract-classes/xapi-xo-controller.mjs';
|
|
20
|
+
const IGNORED_VDIS_TAG = '[NOSNAP]';
|
|
19
21
|
let VmController = class VmController extends XapiXoController {
|
|
20
22
|
constructor(restApi) {
|
|
21
23
|
super('VM', restApi);
|
|
@@ -26,7 +28,7 @@ let VmController = class VmController extends XapiXoController {
|
|
|
26
28
|
* @example filter "power_state:Running"
|
|
27
29
|
* @example limit 42
|
|
28
30
|
*/
|
|
29
|
-
getVms(req, fields, filter, limit) {
|
|
31
|
+
getVms(req, fields, ndjson, filter, limit) {
|
|
30
32
|
return this.sendObjects(Object.values(this.getObjects({ filter, limit })), req);
|
|
31
33
|
}
|
|
32
34
|
/**
|
|
@@ -55,6 +57,52 @@ let VmController = class VmController extends XapiXoController {
|
|
|
55
57
|
throw error;
|
|
56
58
|
}
|
|
57
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* The VM must be running
|
|
62
|
+
*
|
|
63
|
+
* List of possible data_source (Based on [Xenserver doc](https://docs.xenserver.com/en-us/xenserver/8/monitor-performance#available-vm-metrics))
|
|
64
|
+
* - **cpu#** : Utilization of vCPU cpu (fraction). Enabled by default. *Condition*: vCPU cpu exists.
|
|
65
|
+
* - **cpu_usage** : Domain CPU usage. *Condition*: None.
|
|
66
|
+
* - **memory** : Memory currently allocated to VM (Bytes). Enabled by default. *Condition*: None.
|
|
67
|
+
* - **memory_target** : Target of VM balloon driver (Bytes). Enabled by default. *Condition*: None.
|
|
68
|
+
* - **memory_internal_free** : Memory used as reported by the guest agent (KiB). Enabled by default. *Condition*: None.
|
|
69
|
+
* - **runstate_fullrun** : Fraction of time that all vCPUs are running. *Condition*: None.
|
|
70
|
+
* - **runstate_full_contention** : Fraction of time that all vCPUs are runnable (waiting for CPU). *Condition*: None.
|
|
71
|
+
* - **runstate_concurrency_hazard** : Fraction of time that some vCPUs are running and some are runnable. *Condition*: None.
|
|
72
|
+
* - **runstate_blocked** : Fraction of time that all vCPUs are blocked or offline. *Condition*: None.
|
|
73
|
+
* - **runstate_partial_run** : Fraction of time that some vCPUs are running, and some are blocked. *Condition*: None.
|
|
74
|
+
* - **runstate_partial_contention** : Fraction of time that some vCPUs are runnable and some are blocked. *Condition*: None.
|
|
75
|
+
* - **vbd_#_write** : Writes to device vbd in bytes per second. Enabled by default. *Condition*: VBD vbd exists.
|
|
76
|
+
* - **vbd_#_read** : Reads from device vbd in bytes per second. Enabled by default. *Condition*: VBD vbd exists.
|
|
77
|
+
* - **vbd_#_write_latency** : Writes to device vbd in microseconds. *Condition*: VBD vbd exists.
|
|
78
|
+
* - **vbd_#_read_latency** : Reads from device vbd in microseconds. *Condition*: VBD vbd exists.
|
|
79
|
+
* - **vbd_#_iops_read** : Read requests per second. *Condition*: At least one plugged VBD for non-ISO VDI on the host.
|
|
80
|
+
* - **vbd_#_iops_write** : Write requests per second. *Condition*: At least one plugged VBD for non-ISO VDI on the host.
|
|
81
|
+
* - **vbd_#_iops_total** : I/O requests per second. *Condition*: At least one plugged VBD for non-ISO VDI on the host.
|
|
82
|
+
* - **vbd_#_iowait** : Percentage of time waiting for I/O. *Condition*: At least one plugged VBD for non-ISO VDI on the host.
|
|
83
|
+
* - **vbd_#_inflight** : Number of I/O requests currently in flight. *Condition*: At least one plugged VBD for non-ISO VDI on the host.
|
|
84
|
+
* - **vbd_#_avgqu_sz** : Average I/O queue size. *Condition*: At least one plugged VBD for non-ISO VDI on the host.
|
|
85
|
+
* - **vif_#_rx** : Bytes per second received on virtual interface number vif. Enabled by default. *Condition*: VIF vif exists.
|
|
86
|
+
* - **vif_#_tx** : Bytes per second transmitted on virtual interface vif. Enabled by default. *Condition*: VIF vif exists.
|
|
87
|
+
* - **vif_#_rx_errors** : Receive errors per second on virtual interface vif. Enabled by default. *Condition*: VIF vif exists.
|
|
88
|
+
* - **vif_#_tx_errors** : Transmit errors per second on virtual interface vif. Enabled by default. *Condition*: VIF vif exists.
|
|
89
|
+
* @example id "f07ab729-c0e8-721c-45ec-f11276377030"
|
|
90
|
+
* @example dataSource "cpu0"
|
|
91
|
+
*/
|
|
92
|
+
async addDataSource(id, dataSource) {
|
|
93
|
+
await this.getXapiObject(id).$call('record_data_source', dataSource);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The VM must be running
|
|
97
|
+
*
|
|
98
|
+
* For a list of possible data sources, see the endpoint documentation: `GET {id}/stats/data_source/{data_source}`
|
|
99
|
+
*
|
|
100
|
+
* @example id "f07ab729-c0e8-721c-45ec-f11276377030"
|
|
101
|
+
* @example dataSource "cpu0"
|
|
102
|
+
*/
|
|
103
|
+
async deleteDataSource(id, dataSource) {
|
|
104
|
+
await this.getXapiObject(id).$call('forget_data_source_archives', dataSource);
|
|
105
|
+
}
|
|
58
106
|
/**
|
|
59
107
|
* @example id "f07ab729-c0e8-721c-45ec-f11276377030"
|
|
60
108
|
*/
|
|
@@ -140,6 +188,30 @@ let VmController = class VmController extends XapiXoController {
|
|
|
140
188
|
},
|
|
141
189
|
});
|
|
142
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* @example id "f07ab729-c0e8-721c-45ec-f11276377030"
|
|
193
|
+
* @example body { "name_label": "my_awesome_snapshot" }
|
|
194
|
+
*/
|
|
195
|
+
async snapshotVm(id, body, sync) {
|
|
196
|
+
const vmId = id;
|
|
197
|
+
const action = async () => {
|
|
198
|
+
const xapiVm = this.getXapiObject(vmId);
|
|
199
|
+
const ref = await xapiVm.$snapshot({ ignoredVdisTag: IGNORED_VDIS_TAG, name_label: body?.name_label });
|
|
200
|
+
const snapshotId = await xapiVm.$xapi.getField('VM', ref, 'uuid');
|
|
201
|
+
if (sync) {
|
|
202
|
+
this.setHeader('Location', `${BASE_URL}/vm-snapshots/${snapshotId}`);
|
|
203
|
+
}
|
|
204
|
+
return { id: snapshotId };
|
|
205
|
+
};
|
|
206
|
+
return this.createAction(action, {
|
|
207
|
+
sync,
|
|
208
|
+
statusCode: createdResp.status,
|
|
209
|
+
taskProperties: {
|
|
210
|
+
name: 'snapshot VM',
|
|
211
|
+
objectId: vmId,
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
}
|
|
143
215
|
};
|
|
144
216
|
__decorate([
|
|
145
217
|
Example(vmIds),
|
|
@@ -148,7 +220,8 @@ __decorate([
|
|
|
148
220
|
__param(0, Request()),
|
|
149
221
|
__param(1, Query()),
|
|
150
222
|
__param(2, Query()),
|
|
151
|
-
__param(3, Query())
|
|
223
|
+
__param(3, Query()),
|
|
224
|
+
__param(4, Query())
|
|
152
225
|
], VmController.prototype, "getVms", null);
|
|
153
226
|
__decorate([
|
|
154
227
|
Example(vm),
|
|
@@ -164,10 +237,26 @@ __decorate([
|
|
|
164
237
|
__param(0, Path()),
|
|
165
238
|
__param(1, Query())
|
|
166
239
|
], VmController.prototype, "getVmStats", null);
|
|
240
|
+
__decorate([
|
|
241
|
+
SuccessResponse(noContentResp.status, noContentResp.description),
|
|
242
|
+
Response(notFoundResp.status, notFoundResp.description),
|
|
243
|
+
Response(internalServerErrorResp.status, internalServerErrorResp.description),
|
|
244
|
+
Put('{id}/stats/data_source/{data_source}'),
|
|
245
|
+
__param(0, Path()),
|
|
246
|
+
__param(1, Path('data_source'))
|
|
247
|
+
], VmController.prototype, "addDataSource", null);
|
|
248
|
+
__decorate([
|
|
249
|
+
SuccessResponse(noContentResp.status, noContentResp.description),
|
|
250
|
+
Response(notFoundResp.status, notFoundResp.description),
|
|
251
|
+
Response(internalServerErrorResp.status, internalServerErrorResp.description),
|
|
252
|
+
Delete('{id}/stats/data_source/{data_source}'),
|
|
253
|
+
__param(0, Path()),
|
|
254
|
+
__param(1, Path('data_source'))
|
|
255
|
+
], VmController.prototype, "deleteDataSource", null);
|
|
167
256
|
__decorate([
|
|
168
257
|
Example(taskLocation),
|
|
169
258
|
Post('{id}/actions/start'),
|
|
170
|
-
SuccessResponse(
|
|
259
|
+
SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description, asynchronousActionResp.produce),
|
|
171
260
|
Response(noContentResp.status, noContentResp.description),
|
|
172
261
|
Response(notFoundResp.status, notFoundResp.description),
|
|
173
262
|
Response(internalServerErrorResp.status, internalServerErrorResp.description),
|
|
@@ -177,7 +266,7 @@ __decorate([
|
|
|
177
266
|
__decorate([
|
|
178
267
|
Example(taskLocation),
|
|
179
268
|
Post('{id}/actions/clean_shutdown'),
|
|
180
|
-
SuccessResponse(
|
|
269
|
+
SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description, asynchronousActionResp.produce),
|
|
181
270
|
Response(noContentResp.status, noContentResp.description),
|
|
182
271
|
Response(notFoundResp.status, notFoundResp.description),
|
|
183
272
|
Response(internalServerErrorResp.status, internalServerErrorResp.description),
|
|
@@ -193,7 +282,7 @@ __decorate([
|
|
|
193
282
|
__decorate([
|
|
194
283
|
Example(taskLocation),
|
|
195
284
|
Post('{id}/actions/hard_shutdown'),
|
|
196
|
-
SuccessResponse(
|
|
285
|
+
SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description, asynchronousActionResp.produce),
|
|
197
286
|
Response(noContentResp.status, noContentResp.description),
|
|
198
287
|
Response(notFoundResp.status, notFoundResp.description),
|
|
199
288
|
Response(internalServerErrorResp.status, internalServerErrorResp.description),
|
|
@@ -203,13 +292,24 @@ __decorate([
|
|
|
203
292
|
__decorate([
|
|
204
293
|
Example(taskLocation),
|
|
205
294
|
Post('{id}/actions/hard_reboot'),
|
|
206
|
-
SuccessResponse(
|
|
295
|
+
SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description, asynchronousActionResp.produce),
|
|
207
296
|
Response(noContentResp.status, noContentResp.description),
|
|
208
297
|
Response(notFoundResp.status, notFoundResp.description),
|
|
209
298
|
Response(internalServerErrorResp.status, internalServerErrorResp.description),
|
|
210
299
|
__param(0, Path()),
|
|
211
300
|
__param(1, Query())
|
|
212
301
|
], VmController.prototype, "hardRebootVm", null);
|
|
302
|
+
__decorate([
|
|
303
|
+
Example(taskLocation),
|
|
304
|
+
Post('{id}/actions/snapshot'),
|
|
305
|
+
SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description, asynchronousActionResp.produce),
|
|
306
|
+
Response(createdResp.status, 'Snapshot created'),
|
|
307
|
+
Response(notFoundResp.status, notFoundResp.description),
|
|
308
|
+
Response(internalServerErrorResp.status, internalServerErrorResp.description),
|
|
309
|
+
__param(0, Path()),
|
|
310
|
+
__param(1, Body()),
|
|
311
|
+
__param(2, Query())
|
|
312
|
+
], VmController.prototype, "snapshotVm", null);
|
|
213
313
|
VmController = __decorate([
|
|
214
314
|
Route('vms'),
|
|
215
315
|
Security('*'),
|
|
@@ -0,0 +1,39 @@
|
|
|
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 { Controller, Example, Get, Response, Route, Security, Tags } from 'tsoa';
|
|
11
|
+
import { inject } from 'inversify';
|
|
12
|
+
import { provide } from 'inversify-binding-decorators';
|
|
13
|
+
import { unauthorizedResp } from '../open-api/common/response.common.mjs';
|
|
14
|
+
import { xoaDashboard } from '../open-api/oa-examples/xoa.oa-example.mjs';
|
|
15
|
+
import { XoaService } from './xoa.service.mjs';
|
|
16
|
+
let XoaController = class XoaController extends Controller {
|
|
17
|
+
#xoaService;
|
|
18
|
+
constructor(xoaService) {
|
|
19
|
+
super();
|
|
20
|
+
this.#xoaService = xoaService;
|
|
21
|
+
}
|
|
22
|
+
async getDashboard() {
|
|
23
|
+
const dashboard = await this.#xoaService.getDashboard();
|
|
24
|
+
return dashboard;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
__decorate([
|
|
28
|
+
Example(xoaDashboard),
|
|
29
|
+
Get('dashboard')
|
|
30
|
+
], XoaController.prototype, "getDashboard", null);
|
|
31
|
+
XoaController = __decorate([
|
|
32
|
+
Route(''),
|
|
33
|
+
Security('*'),
|
|
34
|
+
Response(unauthorizedResp.status, unauthorizedResp.description),
|
|
35
|
+
Tags('xoa'),
|
|
36
|
+
provide(XoaController),
|
|
37
|
+
__param(0, inject(XoaService))
|
|
38
|
+
], XoaController);
|
|
39
|
+
export { XoaController };
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import groupBy from 'lodash/groupBy.js';
|
|
2
|
+
import semver from 'semver';
|
|
3
|
+
import { BACKUP_TYPE, } from '@vates/types';
|
|
4
|
+
import { asyncEach } from '@vates/async-each';
|
|
5
|
+
import { createLogger } from '@xen-orchestra/log';
|
|
6
|
+
import { createPredicate } from 'value-matcher';
|
|
7
|
+
import { extractIdsFromSimplePattern } from '@xen-orchestra/backups/extractIdsFromSimplePattern.mjs';
|
|
8
|
+
import { noSuchObject } from 'xo-common/api-errors.js';
|
|
9
|
+
import { parse } from 'xo-remote-parser';
|
|
10
|
+
import { getFromAsyncCache } from '../helpers/cache.helper.mjs';
|
|
11
|
+
import { isReplicaVm, isSrWritable, vmContainsNoBakTag } from '../helpers/utils.helper.mjs';
|
|
12
|
+
const log = createLogger('xo:rest-api:xoa-service');
|
|
13
|
+
export class XoaService {
|
|
14
|
+
#restApi;
|
|
15
|
+
#dashboardAsyncCache = new Map();
|
|
16
|
+
#dashboardCacheOpts;
|
|
17
|
+
constructor(restApi) {
|
|
18
|
+
this.#restApi = restApi;
|
|
19
|
+
this.#dashboardCacheOpts = {
|
|
20
|
+
timeout: this.#restApi.xoApp.config.getOptionalDuration('rest-api.dashboardCacheTimeout'),
|
|
21
|
+
expiresIn: this.#restApi.xoApp.config.getOptionalDuration('rest-api.dashboardCacheExpiresIn'),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
async #getBackupRepositoriesSizeInfo() {
|
|
25
|
+
const brResult = await getFromAsyncCache(this.#dashboardAsyncCache, 'backupRepositories', async () => {
|
|
26
|
+
const xoApp = this.#restApi.xoApp;
|
|
27
|
+
const s3Brsize = { backups: 0 };
|
|
28
|
+
const otherBrSize = {
|
|
29
|
+
available: 0,
|
|
30
|
+
backups: 0,
|
|
31
|
+
other: 0,
|
|
32
|
+
total: 0,
|
|
33
|
+
used: 0,
|
|
34
|
+
};
|
|
35
|
+
const backupRepositories = await xoApp.getAllRemotes();
|
|
36
|
+
const backupRepositoriesInfo = await xoApp.getAllRemotesInfo();
|
|
37
|
+
for (const backupRepository of backupRepositories) {
|
|
38
|
+
const { type } = parse(backupRepository.url);
|
|
39
|
+
const backupRepositoryInfo = backupRepositoriesInfo[backupRepository.id];
|
|
40
|
+
if (!backupRepository.enabled || backupRepositoryInfo === undefined) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const totalBackupSize = await xoApp.getTotalBackupSizeOnRemote(backupRepository.id);
|
|
44
|
+
const { available, size, used } = backupRepositoryInfo;
|
|
45
|
+
const isS3 = type === 's3';
|
|
46
|
+
const target = isS3 ? s3Brsize : otherBrSize;
|
|
47
|
+
target.backups += totalBackupSize.onDisk;
|
|
48
|
+
if (!isS3) {
|
|
49
|
+
const _target = target;
|
|
50
|
+
_target.available += available ?? 0;
|
|
51
|
+
_target.other += used - totalBackupSize.onDisk;
|
|
52
|
+
_target.total += size ?? 0;
|
|
53
|
+
_target.used += used;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { s3: { size: s3Brsize }, other: { size: otherBrSize } };
|
|
57
|
+
}, this.#dashboardCacheOpts);
|
|
58
|
+
if (brResult?.value !== undefined) {
|
|
59
|
+
return { ...brResult.value, isExpired: brResult.isExpired };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
#getNumberOfPools() {
|
|
63
|
+
const pools = this.#restApi.getObjectsByType('pool');
|
|
64
|
+
return Object.keys(pools).length;
|
|
65
|
+
}
|
|
66
|
+
#getNumberOfHosts() {
|
|
67
|
+
const hosts = this.#restApi.getObjectsByType('host');
|
|
68
|
+
return Object.keys(hosts).length;
|
|
69
|
+
}
|
|
70
|
+
#getResourcesOverview() {
|
|
71
|
+
const pools = Object.values(this.#restApi.getObjectsByType('pool'));
|
|
72
|
+
const hosts = Object.values(this.#restApi.getObjectsByType('host'));
|
|
73
|
+
const writableSrs = Object.values(this.#restApi.getObjectsByType('SR', {
|
|
74
|
+
filter: isSrWritable,
|
|
75
|
+
}));
|
|
76
|
+
const maxLenght = Math.max(hosts.length, writableSrs.length);
|
|
77
|
+
const resourcesOverview = { nCpus: 0, memorySize: 0, srSize: 0 };
|
|
78
|
+
for (let index = 0; index < maxLenght; index++) {
|
|
79
|
+
const pool = pools[index];
|
|
80
|
+
const host = hosts[index];
|
|
81
|
+
const sr = writableSrs[index];
|
|
82
|
+
if (pool !== undefined) {
|
|
83
|
+
resourcesOverview.nCpus += pool.cpus.cores ?? 0;
|
|
84
|
+
}
|
|
85
|
+
if (host !== undefined) {
|
|
86
|
+
resourcesOverview.memorySize += host.memory.size;
|
|
87
|
+
}
|
|
88
|
+
if (sr !== undefined) {
|
|
89
|
+
resourcesOverview.srSize += sr.size;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return resourcesOverview;
|
|
93
|
+
}
|
|
94
|
+
async #getPoolsStatus() {
|
|
95
|
+
const servers = await this.#restApi.xoApp.getAllXenServers();
|
|
96
|
+
const pools = this.#restApi.getObjectsByType('pool');
|
|
97
|
+
let nConnectedServers = 0;
|
|
98
|
+
let nUnreachableServers = 0;
|
|
99
|
+
let nUnknownServers = 0;
|
|
100
|
+
servers.forEach(server => {
|
|
101
|
+
// it may happen that some servers are marked as "connected", but no pool matches "server.pool"
|
|
102
|
+
// so they are counted as `nUnknownServers`
|
|
103
|
+
if (server.status === 'connected' && server.poolId !== undefined && pools[server.poolId] !== undefined) {
|
|
104
|
+
nConnectedServers++;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (server.status === 'disconnected' &&
|
|
108
|
+
server.error !== undefined &&
|
|
109
|
+
server.error.connectedServerId === undefined) {
|
|
110
|
+
nUnreachableServers++;
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (server.status === 'disconnected') {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
nUnknownServers++;
|
|
117
|
+
});
|
|
118
|
+
return {
|
|
119
|
+
connected: nConnectedServers,
|
|
120
|
+
unreachable: nUnreachableServers,
|
|
121
|
+
unknown: nUnknownServers,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
async #getNumberOfEolHosts() {
|
|
125
|
+
const getHVSupportedVersions = this.#restApi.xoApp.getHVSupportedVersions;
|
|
126
|
+
if (getHVSupportedVersions === undefined) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const hvSupportedVersions = await getHVSupportedVersions();
|
|
130
|
+
const hosts = this.#restApi.getObjectsByType('host');
|
|
131
|
+
let nHostsEol = 0;
|
|
132
|
+
for (const hostId in hosts) {
|
|
133
|
+
const host = hosts[hostId];
|
|
134
|
+
if (!semver.satisfies(host.version, hvSupportedVersions[host.productBrand])) {
|
|
135
|
+
nHostsEol++;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return nHostsEol;
|
|
139
|
+
}
|
|
140
|
+
async #getMissingPatchesInfo() {
|
|
141
|
+
if (!(await this.#restApi.xoApp.hasFeatureAuthorization('LIST_MISSING_PATCHES'))) {
|
|
142
|
+
return {
|
|
143
|
+
hasAuthorization: false,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
const hosts = Object.values(this.#restApi.getObjectsByType('host'));
|
|
147
|
+
const poolsWithMissingPatches = new Set();
|
|
148
|
+
let nHostsWithMissingPatches = 0;
|
|
149
|
+
let nHostsFailed = 0;
|
|
150
|
+
await asyncEach(hosts, async (host) => {
|
|
151
|
+
const xapi = this.#restApi.xoApp.getXapi(host);
|
|
152
|
+
try {
|
|
153
|
+
const patches = await xapi.listMissingPatches(host.id);
|
|
154
|
+
if (patches.length > 0) {
|
|
155
|
+
nHostsWithMissingPatches++;
|
|
156
|
+
poolsWithMissingPatches.add(host.$pool);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
log.error('listMissingPatches failed', err);
|
|
161
|
+
nHostsFailed++;
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
return {
|
|
165
|
+
hasAuthorization: true,
|
|
166
|
+
nHostsFailed,
|
|
167
|
+
nHostsWithMissingPatches,
|
|
168
|
+
nPoolsWithMissingPatches: poolsWithMissingPatches.size,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
#isReplicaVmInVdb(vbds) {
|
|
172
|
+
for (const vbd of vbds) {
|
|
173
|
+
try {
|
|
174
|
+
const vdbObject = this.#restApi.getObject(vbd);
|
|
175
|
+
const { VM } = vdbObject;
|
|
176
|
+
const vmObject = this.#restApi.getObject(VM);
|
|
177
|
+
return isReplicaVm(vmObject);
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
if (!noSuchObject.is(err)) {
|
|
181
|
+
throw err;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
#calculateReplicatedSize(vdiId, cache) {
|
|
188
|
+
if (cache.has(vdiId)) {
|
|
189
|
+
return 0;
|
|
190
|
+
}
|
|
191
|
+
let vdiObject;
|
|
192
|
+
try {
|
|
193
|
+
vdiObject = this.#restApi.getObject(vdiId);
|
|
194
|
+
cache.add(vdiId);
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
if (!noSuchObject.is(err)) {
|
|
198
|
+
throw err;
|
|
199
|
+
}
|
|
200
|
+
return 0;
|
|
201
|
+
}
|
|
202
|
+
const { parent, usage, $VBDs } = vdiObject;
|
|
203
|
+
const replicaUsage = this.#isReplicaVmInVdb($VBDs) && usage ? usage : 0;
|
|
204
|
+
const parentUsage = parent ? this.#calculateReplicatedSize(parent, cache) : 0;
|
|
205
|
+
return replicaUsage + parentUsage;
|
|
206
|
+
}
|
|
207
|
+
#getStorageRepositoriesSizeInfo() {
|
|
208
|
+
const writableSrs = this.#restApi.getObjectsByType('SR', {
|
|
209
|
+
filter: isSrWritable,
|
|
210
|
+
});
|
|
211
|
+
let replicated = 0;
|
|
212
|
+
let total = 0;
|
|
213
|
+
let used = 0;
|
|
214
|
+
for (const srId in writableSrs) {
|
|
215
|
+
const sr = writableSrs[srId];
|
|
216
|
+
const cache = new Set();
|
|
217
|
+
const { VDIs } = sr;
|
|
218
|
+
replicated += VDIs.reduce((total, vdi) => total + this.#calculateReplicatedSize(vdi, cache), 0);
|
|
219
|
+
total += sr.size;
|
|
220
|
+
used += sr.physical_usage;
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
size: { available: total - used, other: used - replicated, replicated, total, used },
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
async #getbackupsInfo() {
|
|
227
|
+
const vmIdsProtected = new Set();
|
|
228
|
+
const vmIdsUnprotected = new Set();
|
|
229
|
+
const nonReplicaVms = Object.values(this.#restApi.getObjectsByType('VM', { filter: vm => !isReplicaVm(vm) }));
|
|
230
|
+
const restApi = this.#restApi;
|
|
231
|
+
const xoApp = restApi.xoApp;
|
|
232
|
+
function _extractVmIdsFromBackupJob(job) {
|
|
233
|
+
let vmIds;
|
|
234
|
+
try {
|
|
235
|
+
vmIds = extractIdsFromSimplePattern(job.vms);
|
|
236
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
237
|
+
}
|
|
238
|
+
catch (_) {
|
|
239
|
+
const predicate = createPredicate(job.vms);
|
|
240
|
+
vmIds = nonReplicaVms.filter(predicate).map(vm => vm.id);
|
|
241
|
+
}
|
|
242
|
+
return vmIds;
|
|
243
|
+
}
|
|
244
|
+
function _processVmsProtection(job, isProtected) {
|
|
245
|
+
if (job.type !== BACKUP_TYPE.backup) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
_extractVmIdsFromBackupJob(job).forEach(vmId => {
|
|
249
|
+
_updateVmProtection(vmId, isProtected);
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
function _updateVmProtection(vmId, isProtected) {
|
|
253
|
+
if (vmIdsProtected.has(vmId) || !xoApp.hasObject(vmId, 'VM')) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const vm = restApi.getObject(vmId, 'VM');
|
|
257
|
+
if (vmContainsNoBakTag(vm)) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (isProtected) {
|
|
261
|
+
vmIdsProtected.add(vmId);
|
|
262
|
+
vmIdsUnprotected.delete(vmId);
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
vmIdsUnprotected.add(vmId);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
async function _jobHasAtLeastOneScheduleEnabled(job) {
|
|
269
|
+
for (const maybeScheduleId in job.settings) {
|
|
270
|
+
if (maybeScheduleId === '') {
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
try {
|
|
274
|
+
const schedule = await xoApp.getSchedule(maybeScheduleId);
|
|
275
|
+
if (schedule.enabled) {
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
if (!noSuchObject.is(error, { id: maybeScheduleId, type: 'schedule' })) {
|
|
281
|
+
console.error(error);
|
|
282
|
+
}
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
const backupsResult = await getFromAsyncCache(this.#dashboardAsyncCache, 'backups', async () => {
|
|
289
|
+
const [logs, jobs] = await Promise.all([
|
|
290
|
+
xoApp.getBackupNgLogsSorted({
|
|
291
|
+
filter: log => log.message === 'backup' || log.message === 'metadata',
|
|
292
|
+
}),
|
|
293
|
+
Promise.all([
|
|
294
|
+
xoApp.getAllJobs('backup'),
|
|
295
|
+
xoApp.getAllJobs('mirrorBackup'),
|
|
296
|
+
xoApp.getAllJobs('metadataBackup'),
|
|
297
|
+
]).then(jobs => jobs.flat(1)),
|
|
298
|
+
]);
|
|
299
|
+
const logsByJob = groupBy(logs, 'jobId');
|
|
300
|
+
let disabledJobs = 0;
|
|
301
|
+
let failedJobs = 0;
|
|
302
|
+
let skippedJobs = 0;
|
|
303
|
+
let successfulJobs = 0;
|
|
304
|
+
const backupJobIssues = [];
|
|
305
|
+
for (const job of jobs) {
|
|
306
|
+
if (!(await _jobHasAtLeastOneScheduleEnabled(job))) {
|
|
307
|
+
_processVmsProtection(job, false);
|
|
308
|
+
disabledJobs++;
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
// Get only the last 3 runs
|
|
312
|
+
const jobLogs = logsByJob[job.id]?.slice(-3).reverse();
|
|
313
|
+
if (jobLogs === undefined || jobLogs.length === 0) {
|
|
314
|
+
_processVmsProtection(job, false);
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
if (job.type === BACKUP_TYPE.backup) {
|
|
318
|
+
const lastJobLog = jobLogs[0];
|
|
319
|
+
const { tasks, status } = lastJobLog;
|
|
320
|
+
if (tasks === undefined) {
|
|
321
|
+
_processVmsProtection(job, status === 'success');
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
// @TODO: remove as when logs are correctly typed
|
|
325
|
+
;
|
|
326
|
+
tasks.forEach(task => {
|
|
327
|
+
_updateVmProtection(task.data.id, task.status === 'success');
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
const failedLog = jobLogs.find(log => log.status !== 'success');
|
|
332
|
+
if (failedLog !== undefined) {
|
|
333
|
+
const { status } = failedLog;
|
|
334
|
+
if (status === 'failure' || status === 'interrupted') {
|
|
335
|
+
failedJobs++;
|
|
336
|
+
}
|
|
337
|
+
else if (status === 'skipped') {
|
|
338
|
+
skippedJobs++;
|
|
339
|
+
}
|
|
340
|
+
backupJobIssues.push({
|
|
341
|
+
// @TODO: remove as when logs are correctly typed
|
|
342
|
+
logs: jobLogs.map(log => log.status),
|
|
343
|
+
name: job.name,
|
|
344
|
+
type: job.type,
|
|
345
|
+
uuid: job.id,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
successfulJobs++;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
const nVmsProtected = vmIdsProtected.size;
|
|
353
|
+
const nVmsUnprotected = vmIdsUnprotected.size;
|
|
354
|
+
const nVmsNotInJob = nonReplicaVms.length - (nVmsProtected + nVmsUnprotected);
|
|
355
|
+
return {
|
|
356
|
+
jobs: {
|
|
357
|
+
disabled: disabledJobs,
|
|
358
|
+
failed: failedJobs,
|
|
359
|
+
skipped: skippedJobs,
|
|
360
|
+
successful: successfulJobs,
|
|
361
|
+
total: jobs.length,
|
|
362
|
+
},
|
|
363
|
+
issues: backupJobIssues,
|
|
364
|
+
vmsProtection: {
|
|
365
|
+
protected: nVmsProtected,
|
|
366
|
+
unprotected: nVmsUnprotected,
|
|
367
|
+
notInJob: nVmsNotInJob,
|
|
368
|
+
},
|
|
369
|
+
};
|
|
370
|
+
}, this.#dashboardCacheOpts);
|
|
371
|
+
if (backupsResult?.value !== undefined) {
|
|
372
|
+
return { ...backupsResult.value, isExpired: backupsResult.isExpired };
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
async getDashboard() {
|
|
376
|
+
const nPools = this.#getNumberOfPools();
|
|
377
|
+
const nHosts = this.#getNumberOfHosts();
|
|
378
|
+
const resourcesOverview = this.#getResourcesOverview();
|
|
379
|
+
const storageRepositories = this.#getStorageRepositoriesSizeInfo();
|
|
380
|
+
const poolsStatus = await this.#getPoolsStatus();
|
|
381
|
+
const missingPatches = await this.#getMissingPatchesInfo();
|
|
382
|
+
const backupRepositories = await this.#getBackupRepositoriesSizeInfo().catch(err => {
|
|
383
|
+
log.error('#getBackupRepositoriesSizeInfo failed', err);
|
|
384
|
+
// explicitly return undefined because typescript understand it as void instead of undefined
|
|
385
|
+
return undefined;
|
|
386
|
+
});
|
|
387
|
+
const nHostsEol = await this.#getNumberOfEolHosts().catch(err => {
|
|
388
|
+
log.err('#getNumberOfEolHosts failed', err);
|
|
389
|
+
return undefined;
|
|
390
|
+
});
|
|
391
|
+
const backups = await this.#getbackupsInfo().catch(err => {
|
|
392
|
+
log.error('#getbackupsInfo failed', err);
|
|
393
|
+
return undefined;
|
|
394
|
+
});
|
|
395
|
+
return {
|
|
396
|
+
nPools,
|
|
397
|
+
nHosts,
|
|
398
|
+
backupRepositories,
|
|
399
|
+
resourcesOverview,
|
|
400
|
+
poolsStatus,
|
|
401
|
+
nHostsEol,
|
|
402
|
+
missingPatches,
|
|
403
|
+
storageRepositories,
|
|
404
|
+
backups,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|