@xen-orchestra/rest-api 0.11.0 → 0.12.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 (30) hide show
  1. package/dist/abstract-classes/base-controller.mjs +2 -2
  2. package/dist/alarms/alarm.controller.mjs +10 -60
  3. package/dist/alarms/alarm.service.mjs +67 -0
  4. package/dist/backup-repositories/backup-repositories.controller.mjs +61 -0
  5. package/dist/groups/group.controller.mjs +15 -2
  6. package/dist/helpers/object-wrapper.helper.mjs +8 -1
  7. package/dist/helpers/utils.helper.mjs +74 -1
  8. package/dist/hosts/host.controller.mjs +57 -2
  9. package/dist/hosts/host.service.mjs +78 -0
  10. package/dist/ioc/ioc.mjs +25 -1
  11. package/dist/messages/message.controller.mjs +1 -1
  12. package/dist/networks/network.controller.mjs +34 -2
  13. package/dist/open-api/oa-examples/alarm.oa-example.mjs +12 -0
  14. package/dist/open-api/oa-examples/backup-repository.oa-example.mjs +31 -0
  15. package/dist/open-api/oa-examples/pool.oa-example.mjs +201 -0
  16. package/dist/open-api/oa-examples/user.oa-example.mjs +1 -0
  17. package/dist/open-api/routes/routes.js +585 -76
  18. package/dist/pifs/pif.controller.mjs +34 -2
  19. package/dist/pools/pool.controller.mjs +70 -3
  20. package/dist/pools/pool.service.mjs +212 -0
  21. package/dist/rest-api/rest-api.mjs +6 -1
  22. package/dist/srs/sr.controller.mjs +34 -2
  23. package/dist/users/user.controller.mjs +32 -3
  24. package/dist/vdi-snapshots/vdi-snapshot.controller.mjs +34 -2
  25. package/dist/vdis/vdi.controller.mjs +34 -2
  26. package/dist/vm-templates/vm-template.controller.mjs +34 -2
  27. package/dist/vms/vm.service.mjs +40 -0
  28. package/dist/xoa/xoa.service.mjs +96 -110
  29. package/open-api/spec/swagger.json +3248 -1133
  30. package/package.json +3 -3
@@ -13,8 +13,8 @@ export class BaseController extends Controller {
13
13
  super();
14
14
  this.restApi = restApi;
15
15
  }
16
- sendObjects(objects, req) {
17
- const mapper = makeObjectMapper(req);
16
+ sendObjects(objects, req, path) {
17
+ const mapper = makeObjectMapper(req, path);
18
18
  const mappedObjects = objects.map(mapper);
19
19
  if (req.query.ndjson === 'true') {
20
20
  const res = req.res;
@@ -7,87 +7,36 @@ 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 * as CM from 'complex-matcher';
11
10
  import { Example, Get, Path, Query, Request, Response, Route, Security, Tags } from 'tsoa';
12
11
  import { inject } from 'inversify';
13
12
  import { noSuchObject } from 'xo-common/api-errors.js';
14
13
  import { provide } from 'inversify-binding-decorators';
15
14
  import { alarm, alarmIds, partialAlarms } from '../open-api/oa-examples/alarm.oa-example.mjs';
16
- import { BASE_URL } from '../index.mjs';
17
15
  import { notFoundResp, unauthorizedResp } from '../open-api/common/response.common.mjs';
18
16
  import { RestApi } from '../rest-api/rest-api.mjs';
19
17
  import { XapiXoController } from '../abstract-classes/xapi-xo-controller.mjs';
20
- // E.g: 'value: 0.6\nconfig:\n<variable>\n<name value="cpu_usage"/>\n<alarm_trigger_level value="0.4"/>\n<alarm_trigger_period value ="60"/>\n</variable>';
21
- const ALARM_BODY_REGEX = /^value:\s*(Infinity|NaN|-Infinity|\d+(?:\.\d+)?)\s*config:\s*<variable>\s*<name value="(.*?)"/;
22
- const ALARM_NAMES = ['ALARM', 'BOND_STATUS_CHANGED', 'MULTIPATH_PERIODIC_ALERT'];
23
- export const alarmPredicate = CM.parse(`name:|(${ALARM_NAMES.join(' ')})`).createPredicate();
18
+ import { AlarmService } from './alarm.service.mjs';
24
19
  let AlarmController = class AlarmController extends XapiXoController {
25
- constructor(restApi) {
20
+ #alarmService;
21
+ constructor(restApi, alarmService) {
26
22
  super('message', restApi);
27
- }
28
- #parseAlarm({ $object, body, ...alarm }) {
29
- let object;
30
- try {
31
- object = this.restApi.getObject($object);
32
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
33
- }
34
- catch (err) {
35
- object = {
36
- type: 'unknown',
37
- uuid: $object,
38
- };
39
- }
40
- let href;
41
- if (object.type !== 'unknown') {
42
- href = `${BASE_URL}/${object.type.toLowerCase() + 's'}/${object.uuid}`;
43
- }
44
- const [, value, name] = body.match(ALARM_BODY_REGEX) ?? [];
45
- return {
46
- ...alarm,
47
- body: {
48
- value, // Keep the value as a string because NaN, Infinity, -Infinity is not valid JSON
49
- name: name ?? body, // for 'BOND_STATUS_CHANGED' and 'MULTIPATH_PERIODIC_ALERT', body is a non-xml string. ("body": "The status of the eth0+eth1 bond is: 1/2 up")
50
- },
51
- object: {
52
- type: object.type,
53
- uuid: object.uuid,
54
- href,
55
- },
56
- };
23
+ this.#alarmService = alarmService;
57
24
  }
58
25
  /**
59
26
  * Override parent getObjects in order to only get `ALARM` messages
60
27
  */
61
- getObjects({ filter, limit = Infinity } = {}) {
62
- const rawAlarms = this.restApi.getObjectsByType('message', {
63
- filter: alarmPredicate,
64
- });
65
- let userFilter = () => true;
66
- if (filter !== undefined) {
67
- userFilter = CM.parse(filter).createPredicate();
68
- }
69
- const alarms = {};
70
- for (const id in rawAlarms) {
71
- if (limit === 0) {
72
- break;
73
- }
74
- const alarm = this.#parseAlarm(rawAlarms[id]);
75
- if (userFilter(alarm)) {
76
- alarms[id] = alarm;
77
- limit--;
78
- }
79
- }
80
- return alarms;
28
+ getObjects(opts) {
29
+ return this.#alarmService.getAlarms(opts);
81
30
  }
82
31
  /**
83
32
  * Override parent getObject in order to only get `ALARM` message
84
33
  */
85
34
  getObject(id) {
86
35
  const maybeAlarm = this.restApi.getObject(id, 'message');
87
- if (!alarmPredicate(maybeAlarm)) {
36
+ if (!this.#alarmService.isAlarm(maybeAlarm)) {
88
37
  throw noSuchObject(id, 'alarm');
89
38
  }
90
- return this.#parseAlarm(maybeAlarm);
39
+ return this.#alarmService.parseAlarm(maybeAlarm);
91
40
  }
92
41
  /**
93
42
  * @example fields "body,id,object"
@@ -126,6 +75,7 @@ AlarmController = __decorate([
126
75
  Response(unauthorizedResp.status, unauthorizedResp.description),
127
76
  Tags('alarms'),
128
77
  provide(AlarmController),
129
- __param(0, inject(RestApi))
78
+ __param(0, inject(RestApi)),
79
+ __param(1, inject(AlarmService))
130
80
  ], AlarmController);
131
81
  export { AlarmController };
@@ -0,0 +1,67 @@
1
+ import * as CM from 'complex-matcher';
2
+ import { BASE_URL } from '../index.mjs';
3
+ // E.g: 'value: 0.6\nconfig:\n<variable>\n<name value="cpu_usage"/>\n<alarm_trigger_level value="0.4"/>\n<alarm_trigger_period value ="60"/>\n</variable>';
4
+ const ALARM_BODY_REGEX = /^value:\s*(Infinity|NaN|-Infinity|\d+(?:\.\d+)?)\s*config:\s*<variable>\s*<name value="(.*?)"/;
5
+ const ALARM_NAMES = ['ALARM', 'BOND_STATUS_CHANGED', 'MULTIPATH_PERIODIC_ALERT'];
6
+ export const alarmPredicate = CM.parse(`name:|(${ALARM_NAMES.join(' ')})`).createPredicate();
7
+ export class AlarmService {
8
+ #restApi;
9
+ constructor(restApi) {
10
+ this.#restApi = restApi;
11
+ }
12
+ isAlarm(maybeAlarm) {
13
+ return alarmPredicate(maybeAlarm);
14
+ }
15
+ parseAlarm({ $object, body, time, ...alarm }) {
16
+ let object;
17
+ try {
18
+ object = this.#restApi.getObject($object);
19
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
20
+ }
21
+ catch (err) {
22
+ object = {
23
+ type: 'unknown',
24
+ uuid: $object,
25
+ };
26
+ }
27
+ let href;
28
+ if (object.type !== 'unknown') {
29
+ href = `${BASE_URL}/${object.type.toLowerCase() + 's'}/${object.uuid}`;
30
+ }
31
+ const [, value, name] = body.match(ALARM_BODY_REGEX) ?? [];
32
+ return {
33
+ ...alarm,
34
+ body: {
35
+ value: Number.isFinite(+value) ? (+value * 100).toFixed(1) : value, // Keep the value as a string because NaN, Infinity, -Infinity is not valid JSON
36
+ name: name ?? body, // for 'BOND_STATUS_CHANGED' and 'MULTIPATH_PERIODIC_ALERT', body is a non-xml string. ("body": "The status of the eth0+eth1 bond is: 1/2 up")
37
+ },
38
+ object: {
39
+ type: object.type,
40
+ uuid: object.uuid,
41
+ href,
42
+ },
43
+ time: time * 1000,
44
+ };
45
+ }
46
+ getAlarms({ filter, limit = Infinity } = {}) {
47
+ const rawAlarms = this.#restApi.getObjectsByType('message', {
48
+ filter: alarmPredicate,
49
+ });
50
+ let userFilter = () => true;
51
+ if (filter !== undefined) {
52
+ userFilter = typeof filter === 'string' ? CM.parse(filter).createPredicate() : filter;
53
+ }
54
+ const alarms = {};
55
+ for (const id in rawAlarms) {
56
+ if (limit === 0) {
57
+ break;
58
+ }
59
+ const alarm = this.parseAlarm(rawAlarms[id]);
60
+ if (userFilter(alarm)) {
61
+ alarms[id] = alarm;
62
+ limit--;
63
+ }
64
+ }
65
+ return alarms;
66
+ }
67
+ }
@@ -0,0 +1,61 @@
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 { Example, Get, Path, Query, Request, Response, Route, Security, Tags } from 'tsoa';
11
+ import { provide } from 'inversify-binding-decorators';
12
+ import { notFoundResp, unauthorizedResp } from '../open-api/common/response.common.mjs';
13
+ import { backupRepositoryIds, partialBackupRepositories, backupRepository, } from '../open-api/oa-examples/backup-repository.oa-example.mjs';
14
+ import { XoController } from '../abstract-classes/xo-controller.mjs';
15
+ let BackupRepositoryController = class BackupRepositoryController extends XoController {
16
+ // --- abstract methods
17
+ getAllCollectionObjects() {
18
+ return this.restApi.xoApp.getAllRemotes();
19
+ }
20
+ getCollectionObject(id) {
21
+ return this.restApi.xoApp.getRemote(id);
22
+ }
23
+ /**
24
+ * @example fields "id,name,enabled"
25
+ * @example filter "enabled?"
26
+ * @example limit 42
27
+ */
28
+ async getRepositories(req, fields, ndjson, filter, limit) {
29
+ return this.sendObjects(Object.values(await this.getObjects({ filter, limit })), req);
30
+ }
31
+ /**
32
+ * @example id "c4284e12-37c9-7967-b9e8-83ef229c3e03"
33
+ */
34
+ getRepository(id) {
35
+ return this.getObject(id);
36
+ }
37
+ };
38
+ __decorate([
39
+ Example(backupRepositoryIds),
40
+ Example(partialBackupRepositories),
41
+ Get(''),
42
+ __param(0, Request()),
43
+ __param(1, Query()),
44
+ __param(2, Query()),
45
+ __param(3, Query()),
46
+ __param(4, Query())
47
+ ], BackupRepositoryController.prototype, "getRepositories", null);
48
+ __decorate([
49
+ Example(backupRepository),
50
+ Get('{id}'),
51
+ Response(notFoundResp.status, notFoundResp.description),
52
+ __param(0, Path())
53
+ ], BackupRepositoryController.prototype, "getRepository", null);
54
+ BackupRepositoryController = __decorate([
55
+ Route('backup-repositories'),
56
+ Security('*'),
57
+ Response(unauthorizedResp.status, unauthorizedResp.description),
58
+ Tags('backup-repositories'),
59
+ provide(BackupRepositoryController)
60
+ ], BackupRepositoryController);
61
+ export { BackupRepositoryController };
@@ -7,9 +7,9 @@ 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 { Delete, Example, Get, Path, Query, Request, Response, Route, Security, SuccessResponse, Tags } from 'tsoa';
11
11
  import { provide } from 'inversify-binding-decorators';
12
- import { notFoundResp, unauthorizedResp } from '../open-api/common/response.common.mjs';
12
+ import { noContentResp, notFoundResp, unauthorizedResp } from '../open-api/common/response.common.mjs';
13
13
  import { group, groupIds, partialGroups } from '../open-api/oa-examples/group.oa-example.mjs';
14
14
  import { XoController } from '../abstract-classes/xo-controller.mjs';
15
15
  let GroupController = class GroupController extends XoController {
@@ -34,6 +34,13 @@ let GroupController = class GroupController extends XoController {
34
34
  getGroup(id) {
35
35
  return this.getObject(id);
36
36
  }
37
+ /**
38
+ * @example id "7d98fee4-3357-41a7-ac3f-9124212badb7"
39
+ */
40
+ async deleteGroup(id) {
41
+ const groupId = id;
42
+ await this.restApi.xoApp.deleteGroup(groupId);
43
+ }
37
44
  };
38
45
  __decorate([
39
46
  Example(groupIds),
@@ -51,6 +58,12 @@ __decorate([
51
58
  Response(notFoundResp.status, notFoundResp.description),
52
59
  __param(0, Path())
53
60
  ], GroupController.prototype, "getGroup", null);
61
+ __decorate([
62
+ Delete('{id}'),
63
+ SuccessResponse(noContentResp.status, noContentResp.description),
64
+ Response(notFoundResp.status, notFoundResp.description),
65
+ __param(0, Path())
66
+ ], GroupController.prototype, "deleteGroup", null);
54
67
  GroupController = __decorate([
55
68
  Route('groups'),
56
69
  Security('*'),
@@ -1,7 +1,14 @@
1
1
  import path from 'node:path';
2
2
  import pick from 'lodash/pick.js';
3
+ import { BASE_URL } from '../index.mjs';
3
4
  const { join } = path.posix;
4
- export function makeObjectMapper(req, path = req.path) {
5
+ export function makeObjectMapper(req, path) {
6
+ if (path === undefined) {
7
+ path = req.path;
8
+ }
9
+ else {
10
+ path = `${BASE_URL}/${path}`;
11
+ }
5
12
  const makeUrl = ({ id }) => join(path, typeof id === 'number' ? String(id) : id);
6
13
  let objectMapper;
7
14
  const { query } = req;
@@ -1,4 +1,77 @@
1
+ import { createLogger } from '@xen-orchestra/log';
2
+ import { isPromise } from 'node:util/types';
1
3
  export const NDJSON_CONTENT_TYPE = 'application/x-ndjson';
2
- export const isSrWritable = (sr) => sr.content_type !== 'iso' && sr.size > 0;
4
+ const log = createLogger('xo:rest-api:utils-helper');
5
+ export const isSrWritable = (sr) => isSrWritableOrIso(sr) && sr.content_type !== 'iso';
6
+ export const isSrWritableOrIso = (sr) => sr.size > 0;
3
7
  export const isReplicaVm = (vm) => 'start' in vm.blockedOperations && vm.other['xo:backup:job'] !== undefined;
4
8
  export const vmContainsNoBakTag = (vm) => vm.tags.some(t => t.split('=', 1)[0] === 'xo:no-bak');
9
+ export const getTopPerProperty = (array, { length = Infinity, prop }) => {
10
+ // avoid mutate original array
11
+ let arr = [...array];
12
+ arr.sort((prev, next) => {
13
+ const prevProp = +prev[prop];
14
+ const nextProp = +next[prop];
15
+ if (typeof prevProp !== 'number' || typeof nextProp !== 'number') {
16
+ throw new Error(`cannot parse: ${prop} as number. ${prev}, ${next}`);
17
+ }
18
+ if (isNaN(prevProp) && isNaN(nextProp)) {
19
+ return 0;
20
+ }
21
+ if (isNaN(prevProp)) {
22
+ return 1;
23
+ }
24
+ if (isNaN(nextProp)) {
25
+ return -1;
26
+ }
27
+ return nextProp - prevProp;
28
+ });
29
+ if (arr.length > length) {
30
+ arr = arr.slice(0, length);
31
+ }
32
+ return arr;
33
+ };
34
+ export async function promiseWriteInStream({ maybePromise, path, stream, handleError = false, }) {
35
+ let data;
36
+ if (isPromise(maybePromise)) {
37
+ try {
38
+ data = await maybePromise;
39
+ }
40
+ catch (err) {
41
+ if (!handleError) {
42
+ throw err;
43
+ }
44
+ log.error(`promiseWriteInStream for ${path} failed`, err);
45
+ data = { error: true };
46
+ }
47
+ }
48
+ else {
49
+ data = maybePromise;
50
+ }
51
+ if (stream !== undefined) {
52
+ if (stream.writableNeedDrain) {
53
+ await new Promise(resolve => stream.once('drain', resolve));
54
+ }
55
+ // handle path like `foo.bar` -> `{foo: {bar: data}}`
56
+ const obj = {};
57
+ let current = obj;
58
+ const keys = path.split('.');
59
+ keys.forEach((key, index) => {
60
+ if (index === keys.length - 1) {
61
+ current[key] = data;
62
+ }
63
+ else {
64
+ current[key] = {};
65
+ current = current[key];
66
+ }
67
+ });
68
+ stream.write(JSON.stringify(obj) + '\n');
69
+ }
70
+ return data;
71
+ }
72
+ export function escapeUnsafeComplexMatcher(maybeString) {
73
+ if (maybeString === undefined || maybeString === '') {
74
+ return maybeString;
75
+ }
76
+ return `(${maybeString})`;
77
+ }
@@ -11,13 +11,18 @@ import { Example, Get, Path, Query, Request, Response, Route, Security, SuccessR
11
11
  import { inject } from 'inversify';
12
12
  import { pipeline } from 'node:stream/promises';
13
13
  import { provide } from 'inversify-binding-decorators';
14
+ import { AlarmService } from '../alarms/alarm.service.mjs';
15
+ import { escapeUnsafeComplexMatcher } from '../helpers/utils.helper.mjs';
16
+ import { genericAlarmsExample } from '../open-api/oa-examples/alarm.oa-example.mjs';
14
17
  import { host, hostIds, hostStats, partialHosts } from '../open-api/oa-examples/host.oa-example.mjs';
15
18
  import { RestApi } from '../rest-api/rest-api.mjs';
16
19
  import { XapiXoController } from '../abstract-classes/xapi-xo-controller.mjs';
17
20
  import { internalServerErrorResp, notFoundResp, unauthorizedResp, } from '../open-api/common/response.common.mjs';
18
21
  let HostController = class HostController extends XapiXoController {
19
- constructor(restApi) {
22
+ #alarmService;
23
+ constructor(restApi, alarmService) {
20
24
  super('host', restApi);
25
+ this.#alarmService = alarmService;
21
26
  }
22
27
  /**
23
28
  * @example fields "id,name_label,productBrand"
@@ -60,6 +65,35 @@ let HostController = class HostController extends XapiXoController {
60
65
  res.setHeaders(headers);
61
66
  await pipeline(response.body, this.maybeCompressResponse(req, res));
62
67
  }
68
+ /**
69
+ * Host must be running
70
+ *
71
+ * Download all logs of a host.
72
+ *
73
+ * @example id "b61a5c92-700e-4966-a13b-00633f03eea8"
74
+ *
75
+ */
76
+ async getHostLogs(req, id) {
77
+ const xapiHost = this.getXapiObject(id);
78
+ const res = req.res;
79
+ const response = await xapiHost.$xapi.getResource('/host_logs_download', { host: xapiHost });
80
+ res.setHeader('Content-Type', 'application/gzip');
81
+ await pipeline(response.body, res);
82
+ }
83
+ /**
84
+ * @example id "b61a5c92-700e-4966-a13b-00633f03eea8"
85
+ * @example fields "id,time"
86
+ * @example filter "time:>1747053793"
87
+ * @example limit 42
88
+ */
89
+ getHostAlarms(req, id, fields, ndjson, filter, limit) {
90
+ const host = this.getObject(id);
91
+ const alarms = this.#alarmService.getAlarms({
92
+ filter: `${escapeUnsafeComplexMatcher(filter) ?? ''} object:uuid:${host.uuid}`,
93
+ limit,
94
+ });
95
+ return this.sendObjects(Object.values(alarms), req, 'alarms');
96
+ }
63
97
  };
64
98
  __decorate([
65
99
  Example(hostIds),
@@ -94,12 +128,33 @@ __decorate([
94
128
  __param(0, Request()),
95
129
  __param(1, Path())
96
130
  ], HostController.prototype, "getAuditLog", null);
131
+ __decorate([
132
+ Get('{id}/logs.tgz'),
133
+ SuccessResponse(200, 'Download started', 'application/gzip'),
134
+ Response(notFoundResp.status, notFoundResp.description),
135
+ Response(internalServerErrorResp.status, internalServerErrorResp.description),
136
+ __param(0, Request()),
137
+ __param(1, Path())
138
+ ], HostController.prototype, "getHostLogs", null);
139
+ __decorate([
140
+ Example(genericAlarmsExample),
141
+ Get('{id}/alarms'),
142
+ Tags('alarms'),
143
+ Response(notFoundResp.status, notFoundResp.description),
144
+ __param(0, Request()),
145
+ __param(1, Path()),
146
+ __param(2, Query()),
147
+ __param(3, Query()),
148
+ __param(4, Query()),
149
+ __param(5, Query())
150
+ ], HostController.prototype, "getHostAlarms", null);
97
151
  HostController = __decorate([
98
152
  Route('hosts'),
99
153
  Security('*'),
100
154
  Response(unauthorizedResp.status, unauthorizedResp.description),
101
155
  Tags('hosts'),
102
156
  provide(HostController),
103
- __param(0, inject(RestApi))
157
+ __param(0, inject(RestApi)),
158
+ __param(1, inject(AlarmService))
104
159
  ], HostController);
105
160
  export { HostController };
@@ -0,0 +1,78 @@
1
+ import { asyncEach } from '@vates/async-each';
2
+ import { createLogger } from '@xen-orchestra/log';
3
+ import { HOST_POWER_STATE } from '@vates/types';
4
+ const log = createLogger('xo:rest-api:host-service');
5
+ export class HostService {
6
+ #restApi;
7
+ constructor(restApi) {
8
+ this.#restApi = restApi;
9
+ }
10
+ getHostsStatus(opts) {
11
+ const hosts = this.#restApi.getObjectsByType('host', opts);
12
+ let nRunning = 0;
13
+ let nHalted = 0;
14
+ let nDisabled = 0;
15
+ let nUnknown = 0;
16
+ let total = 0;
17
+ for (const id in hosts) {
18
+ total++;
19
+ const host = hosts[id];
20
+ if (!host.enabled) {
21
+ nDisabled++;
22
+ continue;
23
+ }
24
+ switch (host.power_state) {
25
+ case HOST_POWER_STATE.RUNNING:
26
+ nRunning++;
27
+ break;
28
+ case HOST_POWER_STATE.HALTED:
29
+ nHalted++;
30
+ break;
31
+ default:
32
+ nUnknown++;
33
+ break;
34
+ }
35
+ }
36
+ return {
37
+ disabled: nDisabled,
38
+ running: nRunning,
39
+ halted: nHalted,
40
+ unknown: nUnknown,
41
+ total,
42
+ };
43
+ }
44
+ async getMissingPatchesInfo(opts) {
45
+ if (!(await this.#restApi.xoApp.hasFeatureAuthorization('LIST_MISSING_PATCHES'))) {
46
+ return {
47
+ hasAuthorization: false,
48
+ };
49
+ }
50
+ const hosts = Object.values(this.#restApi.getObjectsByType('host', opts));
51
+ const missingPatches = new Map();
52
+ const poolsWithMissingPatches = new Set();
53
+ let nHostsWithMissingPatches = 0;
54
+ let nHostsFailed = 0;
55
+ await asyncEach(hosts, async (host) => {
56
+ const xapi = this.#restApi.xoApp.getXapi(host);
57
+ try {
58
+ const patches = await xapi.listMissingPatches(host.id);
59
+ if (patches.length > 0) {
60
+ nHostsWithMissingPatches++;
61
+ poolsWithMissingPatches.add(host.$pool);
62
+ patches.forEach(patch => missingPatches.set(patch.id ?? patch.name, patch));
63
+ }
64
+ }
65
+ catch (err) {
66
+ log.error('listMissingPatches failed', err);
67
+ nHostsFailed++;
68
+ }
69
+ });
70
+ return {
71
+ hasAuthorization: true,
72
+ missingPatches: Array.from(missingPatches.values()),
73
+ nHostsFailed,
74
+ nHostsWithMissingPatches,
75
+ nPoolsWithMissingPatches: poolsWithMissingPatches.size,
76
+ };
77
+ }
78
+ }
package/dist/ioc/ioc.mjs CHANGED
@@ -4,6 +4,9 @@ import { Controller } from 'tsoa';
4
4
  import { RestApi } from '../rest-api/rest-api.mjs';
5
5
  import { VmService } from '../vms/vm.service.mjs';
6
6
  import { XoaService } from '../xoa/xoa.service.mjs';
7
+ import { HostService } from '../hosts/host.service.mjs';
8
+ import { PoolService } from '../pools/pool.service.mjs';
9
+ import { AlarmService } from '../alarms/alarm.service.mjs';
7
10
  const iocContainer = new Container();
8
11
  decorate(injectable(), Controller);
9
12
  iocContainer.load(buildProviderModule());
@@ -13,7 +16,7 @@ export function setupContainer(xoApp) {
13
16
  }
14
17
  iocContainer
15
18
  .bind(RestApi)
16
- .toDynamicValue(() => new RestApi(xoApp))
19
+ .toDynamicValue(() => new RestApi(xoApp, iocContainer))
17
20
  .inSingletonScope();
18
21
  iocContainer
19
22
  .bind(XoaService)
@@ -29,5 +32,26 @@ export function setupContainer(xoApp) {
29
32
  return new VmService(restApi);
30
33
  })
31
34
  .inSingletonScope();
35
+ iocContainer
36
+ .bind(PoolService)
37
+ .toDynamicValue(ctx => {
38
+ const restApi = ctx.container.get(RestApi);
39
+ return new PoolService(restApi);
40
+ })
41
+ .inSingletonScope();
42
+ iocContainer
43
+ .bind(HostService)
44
+ .toDynamicValue(ctx => {
45
+ const restApi = ctx.container.get(RestApi);
46
+ return new HostService(restApi);
47
+ })
48
+ .inSingletonScope();
49
+ iocContainer
50
+ .bind(AlarmService)
51
+ .toDynamicValue(ctx => {
52
+ const restApi = ctx.container.get(RestApi);
53
+ return new AlarmService(restApi);
54
+ })
55
+ .inSingletonScope();
32
56
  }
33
57
  export { iocContainer };
@@ -12,7 +12,7 @@ import { Example, Get, Path, Query, Request, Response, Route, Security, Tags } f
12
12
  import { inject } from 'inversify';
13
13
  import { noSuchObject } from 'xo-common/api-errors.js';
14
14
  import { provide } from 'inversify-binding-decorators';
15
- import { alarmPredicate } from '../alarms/alarm.controller.mjs';
15
+ import { alarmPredicate } from '../alarms/alarm.service.mjs';
16
16
  import { message, messageIds, partialMessages } from '../open-api/oa-examples/message.oa-example.mjs';
17
17
  import { RestApi } from '../rest-api/rest-api.mjs';
18
18
  import { notFoundResp, unauthorizedResp } from '../open-api/common/response.common.mjs';
@@ -10,13 +10,18 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
10
10
  import { Example, Get, Path, Query, Response, Request, Route, Security, Tags, Delete, SuccessResponse } from 'tsoa';
11
11
  import { inject } from 'inversify';
12
12
  import { provide } from 'inversify-binding-decorators';
13
+ import { AlarmService } from '../alarms/alarm.service.mjs';
14
+ import { escapeUnsafeComplexMatcher } from '../helpers/utils.helper.mjs';
15
+ import { genericAlarmsExample } from '../open-api/oa-examples/alarm.oa-example.mjs';
13
16
  import { network, networkIds, partialNetworks } from '../open-api/oa-examples/network.oa-example.mjs';
14
17
  import { noContentResp, notFoundResp, unauthorizedResp } from '../open-api/common/response.common.mjs';
15
18
  import { RestApi } from '../rest-api/rest-api.mjs';
16
19
  import { XapiXoController } from '../abstract-classes/xapi-xo-controller.mjs';
17
20
  let NetworkController = class NetworkController extends XapiXoController {
18
- constructor(restApi) {
21
+ #alarmService;
22
+ constructor(restApi, alarmService) {
19
23
  super('network', restApi);
24
+ this.#alarmService = alarmService;
20
25
  }
21
26
  /**
22
27
  * @example fields "nbd,name_label,id"
@@ -39,6 +44,20 @@ let NetworkController = class NetworkController extends XapiXoController {
39
44
  const networkId = id;
40
45
  await this.getXapiObject(networkId).$xapi.deleteNetwork(networkId);
41
46
  }
47
+ /**
48
+ * @example id "9fe12ca3-d75d-cfb0-492e-cfd2bc6c568f"
49
+ * @example fields "id,time"
50
+ * @example filter "time:>1747053793"
51
+ * @example limit 42
52
+ */
53
+ getNetworkAlarms(req, id, fields, ndjson, filter, limit) {
54
+ const network = this.getObject(id);
55
+ const alarms = this.#alarmService.getAlarms({
56
+ filter: `${escapeUnsafeComplexMatcher(filter) ?? ''} object:uuid:${network.uuid}`,
57
+ limit,
58
+ });
59
+ return this.sendObjects(Object.values(alarms), req, 'alarms');
60
+ }
42
61
  };
43
62
  __decorate([
44
63
  Example(networkIds),
@@ -62,12 +81,25 @@ __decorate([
62
81
  Response(notFoundResp.status, notFoundResp.description),
63
82
  __param(0, Path())
64
83
  ], NetworkController.prototype, "deleteNetwork", null);
84
+ __decorate([
85
+ Example(genericAlarmsExample),
86
+ Get('{id}/alarms'),
87
+ Tags('alarms'),
88
+ Response(notFoundResp.status, notFoundResp.description),
89
+ __param(0, Request()),
90
+ __param(1, Path()),
91
+ __param(2, Query()),
92
+ __param(3, Query()),
93
+ __param(4, Query()),
94
+ __param(5, Query())
95
+ ], NetworkController.prototype, "getNetworkAlarms", null);
65
96
  NetworkController = __decorate([
66
97
  Route('networks'),
67
98
  Security('*'),
68
99
  Response(unauthorizedResp.status, unauthorizedResp.description),
69
100
  Tags('networks'),
70
101
  provide(NetworkController),
71
- __param(0, inject(RestApi))
102
+ __param(0, inject(RestApi)),
103
+ __param(1, inject(AlarmService))
72
104
  ], NetworkController);
73
105
  export { NetworkController };