@xen-orchestra/rest-api 0.8.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.
Files changed (42) hide show
  1. package/README.md +2 -1
  2. package/dist/abstract-classes/base-controller.mjs +14 -2
  3. package/dist/alarms/alarm.controller.mjs +3 -2
  4. package/dist/groups/group.controller.mjs +3 -2
  5. package/dist/helpers/cache.helper.mjs +52 -0
  6. package/dist/helpers/stream.helper.mjs +5 -0
  7. package/dist/helpers/utils.helper.mjs +3 -0
  8. package/dist/hosts/host.controller.mjs +3 -2
  9. package/dist/index.mjs +1 -1
  10. package/dist/ioc/ioc.mjs +8 -0
  11. package/dist/messages/message.controller.mjs +3 -2
  12. package/dist/middlewares/generic-error-handler.middleware.mjs +5 -1
  13. package/dist/networks/network.controller.mjs +18 -4
  14. package/dist/open-api/common/response.common.mjs +1 -1
  15. package/dist/open-api/oa-examples/pci.oa-example.mjs +30 -0
  16. package/dist/open-api/oa-examples/pgpu.oa-example.mjs +36 -0
  17. package/dist/open-api/oa-examples/schedule.oa-example.mjs +3 -0
  18. package/dist/open-api/oa-examples/vm-controller.oa-example.mjs +1 -1
  19. package/dist/open-api/oa-examples/xoa.oa-example.mjs +61 -0
  20. package/dist/open-api/routes/routes.js +589 -23
  21. package/dist/pcis/pci.controller.mjs +60 -0
  22. package/dist/pgpus/pgpu.controller.mjs +60 -0
  23. package/dist/pifs/pif.controller.mjs +3 -2
  24. package/dist/pools/pool.controller.mjs +136 -4
  25. package/dist/pools/pool.type.mjs +1 -0
  26. package/dist/schedules/schedule.controller.mjs +5 -4
  27. package/dist/servers/server.controller.mjs +19 -6
  28. package/dist/srs/sr.controller.mjs +3 -2
  29. package/dist/users/user.controller.mjs +3 -2
  30. package/dist/vbds/vbd.controller.mjs +3 -2
  31. package/dist/vdi-snapshots/vdi-snapshot.controller.mjs +3 -2
  32. package/dist/vdis/vdi.controller.mjs +3 -2
  33. package/dist/vifs/vif.controller.mjs +3 -2
  34. package/dist/vm-controller/vm-controller.controller.mjs +3 -2
  35. package/dist/vm-snapshots/vm-snapshot.controller.mjs +3 -2
  36. package/dist/vm-templates/vm-template.controller.mjs +3 -2
  37. package/dist/vms/vm.controller.mjs +72 -9
  38. package/dist/xoa/xoa.controller.mjs +39 -0
  39. package/dist/xoa/xoa.service.mjs +407 -0
  40. package/dist/xoa/xoa.type.mjs +1 -0
  41. package/open-api/spec/swagger.json +4611 -2867
  42. package/package.json +10 -4
@@ -7,11 +7,11 @@ 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 } 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 { actionAsyncroneResp, createdResp, internalServerErrorResp, noContentResp, notFoundResp, unauthorizedResp, } from '../open-api/common/response.common.mjs';
14
+ import { asynchronousActionResp, createdResp, internalServerErrorResp, noContentResp, notFoundResp, unauthorizedResp, } from '../open-api/common/response.common.mjs';
15
15
  import { BASE_URL } from '../index.mjs';
16
16
  import { partialVms, vm, vmIds, vmStatsExample } from '../open-api/oa-examples/vm.oa-example.mjs';
17
17
  import { RestApi } from '../rest-api/rest-api.mjs';
@@ -28,7 +28,7 @@ let VmController = class VmController extends XapiXoController {
28
28
  * @example filter "power_state:Running"
29
29
  * @example limit 42
30
30
  */
31
- getVms(req, fields, filter, limit) {
31
+ getVms(req, fields, ndjson, filter, limit) {
32
32
  return this.sendObjects(Object.values(this.getObjects({ filter, limit })), req);
33
33
  }
34
34
  /**
@@ -57,6 +57,52 @@ let VmController = class VmController extends XapiXoController {
57
57
  throw error;
58
58
  }
59
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
+ }
60
106
  /**
61
107
  * @example id "f07ab729-c0e8-721c-45ec-f11276377030"
62
108
  */
@@ -174,7 +220,8 @@ __decorate([
174
220
  __param(0, Request()),
175
221
  __param(1, Query()),
176
222
  __param(2, Query()),
177
- __param(3, Query())
223
+ __param(3, Query()),
224
+ __param(4, Query())
178
225
  ], VmController.prototype, "getVms", null);
179
226
  __decorate([
180
227
  Example(vm),
@@ -190,10 +237,26 @@ __decorate([
190
237
  __param(0, Path()),
191
238
  __param(1, Query())
192
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);
193
256
  __decorate([
194
257
  Example(taskLocation),
195
258
  Post('{id}/actions/start'),
196
- SuccessResponse(actionAsyncroneResp.status, actionAsyncroneResp.description, actionAsyncroneResp.produce),
259
+ SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description, asynchronousActionResp.produce),
197
260
  Response(noContentResp.status, noContentResp.description),
198
261
  Response(notFoundResp.status, notFoundResp.description),
199
262
  Response(internalServerErrorResp.status, internalServerErrorResp.description),
@@ -203,7 +266,7 @@ __decorate([
203
266
  __decorate([
204
267
  Example(taskLocation),
205
268
  Post('{id}/actions/clean_shutdown'),
206
- SuccessResponse(actionAsyncroneResp.status, actionAsyncroneResp.description, actionAsyncroneResp.produce),
269
+ SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description, asynchronousActionResp.produce),
207
270
  Response(noContentResp.status, noContentResp.description),
208
271
  Response(notFoundResp.status, notFoundResp.description),
209
272
  Response(internalServerErrorResp.status, internalServerErrorResp.description),
@@ -219,7 +282,7 @@ __decorate([
219
282
  __decorate([
220
283
  Example(taskLocation),
221
284
  Post('{id}/actions/hard_shutdown'),
222
- SuccessResponse(actionAsyncroneResp.status, actionAsyncroneResp.description, actionAsyncroneResp.produce),
285
+ SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description, asynchronousActionResp.produce),
223
286
  Response(noContentResp.status, noContentResp.description),
224
287
  Response(notFoundResp.status, notFoundResp.description),
225
288
  Response(internalServerErrorResp.status, internalServerErrorResp.description),
@@ -229,7 +292,7 @@ __decorate([
229
292
  __decorate([
230
293
  Example(taskLocation),
231
294
  Post('{id}/actions/hard_reboot'),
232
- SuccessResponse(actionAsyncroneResp.status, actionAsyncroneResp.description, actionAsyncroneResp.produce),
295
+ SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description, asynchronousActionResp.produce),
233
296
  Response(noContentResp.status, noContentResp.description),
234
297
  Response(notFoundResp.status, notFoundResp.description),
235
298
  Response(internalServerErrorResp.status, internalServerErrorResp.description),
@@ -239,7 +302,7 @@ __decorate([
239
302
  __decorate([
240
303
  Example(taskLocation),
241
304
  Post('{id}/actions/snapshot'),
242
- SuccessResponse(actionAsyncroneResp.status, actionAsyncroneResp.description, actionAsyncroneResp.produce),
305
+ SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description, asynchronousActionResp.produce),
243
306
  Response(createdResp.status, 'Snapshot created'),
244
307
  Response(notFoundResp.status, notFoundResp.description),
245
308
  Response(internalServerErrorResp.status, internalServerErrorResp.description),
@@ -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 {};