@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.
Files changed (43) 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/pif.oa-example.mjs +51 -0
  18. package/dist/open-api/oa-examples/schedule.oa-example.mjs +3 -0
  19. package/dist/open-api/oa-examples/vm-controller.oa-example.mjs +1 -1
  20. package/dist/open-api/oa-examples/xoa.oa-example.mjs +61 -0
  21. package/dist/open-api/routes/routes.js +717 -23
  22. package/dist/pcis/pci.controller.mjs +60 -0
  23. package/dist/pgpus/pgpu.controller.mjs +60 -0
  24. package/dist/pifs/pif.controller.mjs +60 -0
  25. package/dist/pools/pool.controller.mjs +136 -4
  26. package/dist/pools/pool.type.mjs +1 -0
  27. package/dist/schedules/schedule.controller.mjs +5 -4
  28. package/dist/servers/server.controller.mjs +19 -6
  29. package/dist/srs/sr.controller.mjs +3 -2
  30. package/dist/users/user.controller.mjs +3 -2
  31. package/dist/vbds/vbd.controller.mjs +3 -2
  32. package/dist/vdi-snapshots/vdi-snapshot.controller.mjs +3 -2
  33. package/dist/vdis/vdi.controller.mjs +3 -2
  34. package/dist/vifs/vif.controller.mjs +3 -2
  35. package/dist/vm-controller/vm-controller.controller.mjs +3 -2
  36. package/dist/vm-snapshots/vm-snapshot.controller.mjs +3 -2
  37. package/dist/vm-templates/vm-template.controller.mjs +3 -2
  38. package/dist/vms/vm.controller.mjs +108 -8
  39. package/dist/xoa/xoa.controller.mjs +39 -0
  40. package/dist/xoa/xoa.service.mjs +407 -0
  41. package/dist/xoa/xoa.type.mjs +1 -0
  42. package/open-api/spec/swagger.json +5200 -2606
  43. 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 { actionAsyncroneResp, 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
+ 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(actionAsyncroneResp.status, actionAsyncroneResp.description, actionAsyncroneResp.produce),
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(actionAsyncroneResp.status, actionAsyncroneResp.description, actionAsyncroneResp.produce),
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(actionAsyncroneResp.status, actionAsyncroneResp.description, actionAsyncroneResp.produce),
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(actionAsyncroneResp.status, actionAsyncroneResp.description, actionAsyncroneResp.produce),
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 {};