@xen-orchestra/rest-api 0.13.1 → 0.15.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 (33) hide show
  1. package/dist/abstract-classes/xo-controller.mjs +3 -9
  2. package/dist/helpers/error.helper.mjs +10 -0
  3. package/dist/helpers/object-wrapper.helper.mjs +10 -7
  4. package/dist/helpers/utils.helper.mjs +11 -0
  5. package/dist/hosts/host.controller.mjs +42 -4
  6. package/dist/hosts/host.service.mjs +6 -7
  7. package/dist/hosts/host.type.mjs +1 -0
  8. package/dist/index.mjs +4 -0
  9. package/dist/middlewares/generic-error-handler.middleware.mjs +5 -1
  10. package/dist/open-api/common/response.common.mjs +4 -0
  11. package/dist/open-api/oa-examples/host.oa-example.mjs +31 -0
  12. package/dist/open-api/oa-examples/pool.oa-example.mjs +30 -0
  13. package/dist/open-api/oa-examples/task.oa-example.mjs +46 -0
  14. package/dist/open-api/oa-examples/vm-controller.oa-example.mjs +14 -0
  15. package/dist/open-api/oa-examples/vm-snapshot.oa-example.mjs +14 -0
  16. package/dist/open-api/oa-examples/vm-template.oa-example.mjs +14 -0
  17. package/dist/open-api/oa-examples/vm.oa-example.mjs +14 -0
  18. package/dist/open-api/routes/routes.js +364 -47
  19. package/dist/pools/pool.controller.mjs +16 -1
  20. package/dist/pools/pool.service.mjs +18 -8
  21. package/dist/rest-api/rest-api.mjs +10 -2
  22. package/dist/tasks/task.controller.mjs +121 -0
  23. package/dist/users/user.controller.mjs +0 -3
  24. package/dist/vm-controller/vm-controller.controller.mjs +30 -4
  25. package/dist/vm-snapshots/vm-snapshot.controller.mjs +30 -4
  26. package/dist/vm-templates/vm-template.controller.mjs +30 -4
  27. package/dist/vms/vm.controller.mjs +30 -4
  28. package/dist/vms/vm.service.mjs +14 -1
  29. package/dist/xoa/xoa.service.mjs +14 -4
  30. package/open-api/spec/swagger.json +1919 -499
  31. package/package.json +5 -4
  32. package/tsconfig.json +1 -0
  33. package/tsoa.json +12 -0
@@ -7,23 +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 * as CM from 'complex-matcher';
11
10
  import { inject } from 'inversify';
12
11
  import { BaseController } from './base-controller.mjs';
13
12
  import { RestApi } from '../rest-api/rest-api.mjs';
13
+ import { limitAndFilterArray } from '../helpers/utils.helper.mjs';
14
14
  let XoController = class XoController extends BaseController {
15
15
  constructor(restApi) {
16
16
  super(restApi);
17
17
  }
18
- async getObjects({ filter, limit = Infinity } = {}) {
18
+ async getObjects(opts = {}) {
19
19
  let objects = await this.getAllCollectionObjects();
20
- if (filter !== undefined) {
21
- const predicate = CM.parse(filter).createPredicate();
22
- objects = objects.filter(predicate);
23
- }
24
- if (limit < objects.length) {
25
- objects.length = limit;
26
- }
20
+ objects = limitAndFilterArray(objects, opts);
27
21
  const objectById = {};
28
22
  objects.forEach(obj => {
29
23
  objectById[obj.id] = obj;
@@ -0,0 +1,10 @@
1
+ export class ApiError extends Error {
2
+ #status;
3
+ constructor(message, status) {
4
+ super(message);
5
+ this.#status = status;
6
+ }
7
+ get status() {
8
+ return this.#status;
9
+ }
10
+ }
@@ -3,13 +3,16 @@ import pick from 'lodash/pick.js';
3
3
  import { BASE_URL } from '../index.mjs';
4
4
  const { join } = path.posix;
5
5
  export function makeObjectMapper(req, path) {
6
- if (path === undefined) {
7
- path = req.path;
8
- }
9
- else {
10
- path = `${BASE_URL}/${path}`;
11
- }
12
- const makeUrl = ({ id }) => join(path, typeof id === 'number' ? String(id) : id);
6
+ const makeUrl = (obj) => {
7
+ let _path;
8
+ if (path === undefined) {
9
+ _path = req.path;
10
+ }
11
+ else {
12
+ _path = `${BASE_URL}/${typeof path === 'string' ? path : path(obj)}`;
13
+ }
14
+ return join(_path, typeof obj.id === 'number' ? String(obj.id) : obj.id);
15
+ };
13
16
  let objectMapper;
14
17
  const { query } = req;
15
18
  const { fields } = query;
@@ -1,3 +1,4 @@
1
+ import * as CM from 'complex-matcher';
1
2
  import { createLogger } from '@xen-orchestra/log';
2
3
  import { isPromise } from 'node:util/types';
3
4
  export const NDJSON_CONTENT_TYPE = 'application/x-ndjson';
@@ -75,3 +76,13 @@ export function escapeUnsafeComplexMatcher(maybeString) {
75
76
  }
76
77
  return `(${maybeString})`;
77
78
  }
79
+ export function limitAndFilterArray(array, { filter, limit = Infinity } = {}) {
80
+ if (filter !== undefined) {
81
+ const predicate = typeof filter === 'string' ? CM.parse(filter).createPredicate() : filter;
82
+ array = array.filter(predicate);
83
+ }
84
+ if (limit < array.length) {
85
+ array = array.slice(0, limit);
86
+ }
87
+ return array;
88
+ }
@@ -14,15 +14,18 @@ import { provide } from 'inversify-binding-decorators';
14
14
  import { AlarmService } from '../alarms/alarm.service.mjs';
15
15
  import { escapeUnsafeComplexMatcher } from '../helpers/utils.helper.mjs';
16
16
  import { genericAlarmsExample } from '../open-api/oa-examples/alarm.oa-example.mjs';
17
- import { host, hostIds, hostStats, partialHosts } from '../open-api/oa-examples/host.oa-example.mjs';
17
+ import { host, hostIds, hostSmt, hostMissingPatches, hostStats, partialHosts, } from '../open-api/oa-examples/host.oa-example.mjs';
18
18
  import { RestApi } from '../rest-api/rest-api.mjs';
19
19
  import { XapiXoController } from '../abstract-classes/xapi-xo-controller.mjs';
20
- import { internalServerErrorResp, notFoundResp, unauthorizedResp, } from '../open-api/common/response.common.mjs';
20
+ import { featureUnauthorized, internalServerErrorResp, notFoundResp, unauthorizedResp, } from '../open-api/common/response.common.mjs';
21
+ import { HostService } from './host.service.mjs';
21
22
  let HostController = class HostController extends XapiXoController {
22
23
  #alarmService;
23
- constructor(restApi, alarmService) {
24
+ #hostService;
25
+ constructor(restApi, alarmService, hostService) {
24
26
  super('host', restApi);
25
27
  this.#alarmService = alarmService;
28
+ this.#hostService = hostService;
26
29
  }
27
30
  /**
28
31
  * @example fields "id,name_label,productBrand"
@@ -94,6 +97,26 @@ let HostController = class HostController extends XapiXoController {
94
97
  });
95
98
  return this.sendObjects(Object.values(alarms), req, 'alarms');
96
99
  }
100
+ /**
101
+ * Returns a boolean indicating whether SMT (Simultaneous Multi-Threading) is enabled
102
+ *
103
+ * @example id "b61a5c92-700e-4966-a13b-00633f03eea8"
104
+ */
105
+ async gethostSmt(id) {
106
+ const hostId = id;
107
+ const xapiHost = this.getXapiObject(hostId);
108
+ const enabled = Boolean(await xapiHost.$xapi.isHyperThreadingEnabled(hostId));
109
+ return { enabled };
110
+ }
111
+ /**
112
+ * Host must be running
113
+ *
114
+ * @example id "b61a5c92-700e-4966-a13b-00633f03eea8"
115
+ */
116
+ async getMissingPatches(id) {
117
+ const { missingPatches } = await this.#hostService.getMissingPatchesInfo({ filter: host => host.id === id });
118
+ return missingPatches;
119
+ }
97
120
  };
98
121
  __decorate([
99
122
  Example(hostIds),
@@ -148,6 +171,20 @@ __decorate([
148
171
  __param(4, Query()),
149
172
  __param(5, Query())
150
173
  ], HostController.prototype, "getHostAlarms", null);
174
+ __decorate([
175
+ Example(hostSmt),
176
+ Get('{id}/smt'),
177
+ Response(notFoundResp.status, notFoundResp.description),
178
+ Response(internalServerErrorResp.status, internalServerErrorResp.description),
179
+ __param(0, Path())
180
+ ], HostController.prototype, "gethostSmt", null);
181
+ __decorate([
182
+ Example(hostMissingPatches),
183
+ Get('{id}/missing_patches'),
184
+ Response(notFoundResp.status, notFoundResp.description),
185
+ Response(featureUnauthorized.status, featureUnauthorized.description),
186
+ __param(0, Path())
187
+ ], HostController.prototype, "getMissingPatches", null);
151
188
  HostController = __decorate([
152
189
  Route('hosts'),
153
190
  Security('*'),
@@ -155,6 +192,7 @@ HostController = __decorate([
155
192
  Tags('hosts'),
156
193
  provide(HostController),
157
194
  __param(0, inject(RestApi)),
158
- __param(1, inject(AlarmService))
195
+ __param(1, inject(AlarmService)),
196
+ __param(2, inject(HostService))
159
197
  ], HostController);
160
198
  export { HostController };
@@ -41,13 +41,12 @@ export class HostService {
41
41
  total,
42
42
  };
43
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));
44
+ /**
45
+ * Throw if no authorization
46
+ */
47
+ async getMissingPatchesInfo({ filter, } = {}) {
48
+ await this.#restApi.xoApp.checkFeatureAuthorization('LIST_MISSING_PATCHES');
49
+ const hosts = Object.values(this.#restApi.getObjectsByType('host', { filter }));
51
50
  const missingPatches = new Map();
52
51
  const poolsWithMissingPatches = new Set();
53
52
  let nHostsWithMissingPatches = 0;
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.mjs CHANGED
@@ -32,6 +32,10 @@ const SWAGGER_UI_OPTIONS = {
32
32
  export default function setupRestApi(express, xoApp) {
33
33
  setupContainer(xoApp);
34
34
  RegisterRoutes(express);
35
+ express.get(`${BASE_URL}/docs/swagger.json`, (_req, res) => {
36
+ res.setHeader('Content-Type', 'application/json');
37
+ res.json(swaggerOpenApiSpec);
38
+ });
35
39
  // do not register the doc at the root level, or it may lead to unwanted behaviour
36
40
  // uncomment when all endpoints are migrated to this API
37
41
  // express.get('/rest/v0', (_req, res) => res.redirect('/rest/v0/docs'))
@@ -1,5 +1,6 @@
1
1
  import { createLogger } from '@xen-orchestra/log';
2
2
  import { featureUnauthorized, forbiddenOperation, incorrectState, invalidCredentials, invalidParameters, noSuchObject, notImplemented, objectAlreadyExists, unauthorized, } from 'xo-common/api-errors.js';
3
+ import { ApiError } from '../helpers/error.helper.mjs';
3
4
  const log = createLogger('xo:rest-api:error-handler');
4
5
  // must have 4 parameters to be recognized as an error middleware by express
5
6
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -14,7 +15,10 @@ export default function genericErrorHandler(error, req, res, _next) {
14
15
  data: 'data' in error ? error.data : undefined,
15
16
  };
16
17
  let statusCode;
17
- if (noSuchObject.is(error)) {
18
+ if (error instanceof ApiError) {
19
+ statusCode = error.status;
20
+ }
21
+ else if (noSuchObject.is(error)) {
18
22
  statusCode = 404;
19
23
  }
20
24
  else if (unauthorized.is(error) || forbiddenOperation.is(error)) {
@@ -39,3 +39,7 @@ export const forbiddenOperationResp = {
39
39
  status: 403,
40
40
  description: 'Forbidden',
41
41
  };
42
+ export const badRequestResp = {
43
+ status: 400,
44
+ description: 'Bad request',
45
+ };
@@ -775,3 +775,34 @@ export const hostStats = {
775
775
  },
776
776
  },
777
777
  };
778
+ export const hostSmt = { enabled: true };
779
+ export const hostMissingPatches = [
780
+ {
781
+ url: 'http://www.samba.org/',
782
+ version: '4.10.16',
783
+ name: 'libsmbclient',
784
+ license: 'GPLv3+ and LGPLv3+',
785
+ changelog: {
786
+ date: 1690286400,
787
+ description: '- resolves: #2222250 - Fix netlogon capabilities level 2',
788
+ author: 'Andreas Schneider <asn@redhat.com> - 4.10.16-25',
789
+ },
790
+ release: '25.el7_9',
791
+ size: 149400,
792
+ description: 'The SMB client library',
793
+ },
794
+ {
795
+ url: 'http://www.openssh.com/portable.html',
796
+ version: '7.4p1',
797
+ name: 'openssh',
798
+ license: 'BSD',
799
+ changelog: {
800
+ date: 1742212800,
801
+ description: '- Fix CVE-2025-26465 - Fix cases where error codes were not correctly set',
802
+ author: 'Lucas Ravagnier <lucas.ravagnier@vates.tech> - 7.4p1-23.3.2 + 0.10.3-2.23.3.2',
803
+ },
804
+ release: '23.3.2.xcpng8.2',
805
+ size: 429044,
806
+ description: 'An open source implementation of SSH protocol versions 1 and 2',
807
+ },
808
+ ];
@@ -473,3 +473,33 @@ export const poolDashboard = {
473
473
  percent: 8.333333333333334,
474
474
  },
475
475
  };
476
+ export const poolMissingPatches = [
477
+ {
478
+ url: 'http://www.samba.org/',
479
+ version: '4.10.16',
480
+ name: 'libsmbclient',
481
+ license: 'GPLv3+ and LGPLv3+',
482
+ changelog: {
483
+ date: 1690286400,
484
+ description: '- resolves: #2222250 - Fix netlogon capabilities level 2',
485
+ author: 'Andreas Schneider <asn@redhat.com> - 4.10.16-25',
486
+ },
487
+ release: '25.el7_9',
488
+ size: 149400,
489
+ description: 'The SMB client library',
490
+ },
491
+ {
492
+ url: 'http://www.openssh.com/portable.html',
493
+ version: '7.4p1',
494
+ name: 'openssh',
495
+ license: 'BSD',
496
+ changelog: {
497
+ date: 1742212800,
498
+ description: '- Fix CVE-2025-26465 - Fix cases where error codes were not correctly set',
499
+ author: 'Lucas Ravagnier <lucas.ravagnier@vates.tech> - 7.4p1-23.3.2 + 0.10.3-2.23.3.2',
500
+ },
501
+ release: '23.3.2.xcpng8.2',
502
+ size: 429044,
503
+ description: 'An open source implementation of SSH protocol versions 1 and 2',
504
+ },
505
+ ];
@@ -1 +1,47 @@
1
1
  export const taskLocation = '/rest/v0/tasks/0m7kl0j9l';
2
+ export const taskIds = ['/rest/v0/tasks/0mdd1basu', '/rest/v0/tasks/0mdd1t24g'];
3
+ export const partialTasks = [
4
+ {
5
+ status: 'failure',
6
+ id: '0mdd1basu',
7
+ properties: {
8
+ method: 'xoa.licenses.getSelf',
9
+ params: {},
10
+ name: 'API call: xoa.licenses.getSelf',
11
+ userId: 'e531b8c9-3876-4ed9-8fd2-0476d5f825c9',
12
+ type: 'api.call',
13
+ },
14
+ href: '/rest/v0/tasks/0mdd1basu',
15
+ },
16
+ {
17
+ status: 'failure',
18
+ id: '0mdd1t24g',
19
+ properties: {
20
+ method: 'xoa.licenses.getSelf',
21
+ params: {},
22
+ name: 'API call: xoa.licenses.getSelf',
23
+ userId: 'e531b8c9-3876-4ed9-8fd2-0476d5f825c9',
24
+ type: 'api.call',
25
+ },
26
+ href: '/rest/v0/tasks/0mdd1t24g',
27
+ },
28
+ ];
29
+ export const task = {
30
+ id: '0mdd1basu',
31
+ properties: {
32
+ method: 'xoa.licenses.getSelf',
33
+ params: {},
34
+ name: 'API call: xoa.licenses.getSelf',
35
+ userId: 'e531b8c9-3876-4ed9-8fd2-0476d5f825c9',
36
+ type: 'api.call',
37
+ },
38
+ start: 1753098047598,
39
+ status: 'failure',
40
+ updatedAt: 1753098047696,
41
+ end: 1753098047600,
42
+ result: {
43
+ message: 'invalid status closed, expected open',
44
+ name: 'ConnectionError',
45
+ stack: 'ConnectionError: invalid status closed, expected open\n at JsonRpcWebSocketClient._assertStatus (/home/debian/xoa/node_modules/jsonrpc-websocket-client/src/websocket-client.js:141:13)\n at JsonRpcWebSocketClient.send (/home/debian/xoa/node_modules/jsonrpc-websocket-client/src/websocket-client.js:128:10)\n at Peer.<anonymous> (/home/debian/xoa/node_modules/jsonrpc-websocket-client/src/index.js:47:12)\n at Peer.emit (node:events:518:28)\n at Peer.emit (/home/debian/xen-orchestra/@xen-orchestra/log/configure.js:52:17)\n at Peer.push (/home/debian/xoa/node_modules/json-rpc-peer/src/index.js:196:52)\n at /home/debian/xoa/node_modules/json-rpc-peer/src/index.js:142:12\n at Promise._execute (/home/debian/xen-orchestra/node_modules/bluebird/js/release/debuggability.js:384:9)\n at Promise._resolveFromExecutor (/home/debian/xen-orchestra/node_modules/bluebird/js/release/promise.js:518:18)\n at new Promise (/home/debian/xen-orchestra/node_modules/bluebird/js/release/promise.js:103:10)\n at Peer.request (/home/debian/xoa/node_modules/json-rpc-peer/src/index.js:139:12)\n at JsonRpcWebSocketClient.call (/home/debian/xoa/node_modules/jsonrpc-websocket-client/src/index.js:63:23)\n at Xoa.apply [as _getSelfLicenses] (/home/debian/xoa/packages/xo-server-xoa/src/index.js:929:26)\n at Xo.call (file:///home/debian/xen-orchestra/packages/xo-server/src/xo-mixins/api.mjs:269:25)\n at file:///home/debian/xen-orchestra/packages/xo-server/src/xo-mixins/api.mjs:421:33\n at AsyncLocalStorage.run (node:internal/async_local_storage/async_hooks:91:14)\n at Task.runInside (/home/debian/xen-orchestra/@vates/task/index.js:175:41)\n at Task.run (/home/debian/xen-orchestra/@vates/task/index.js:159:31)\n at run (file:///home/debian/xen-orchestra/packages/xo-server/src/xo-mixins/api.mjs:421:16)\n at Api.#callApiMethod (file:///home/debian/xen-orchestra/packages/xo-server/src/xo-mixins/api.mjs:469:24)',
46
+ },
47
+ };
@@ -69,3 +69,17 @@ export const vmController = {
69
69
  $poolId: 'b7569d99-30f8-178a-7d94-801de3e29b5b',
70
70
  _xapiRef: 'OpaqueRef:ca27fcfc-5083-d039-e752-2e6c3364bde9',
71
71
  };
72
+ export const vmControllerVdis = [
73
+ {
74
+ VDI_type: 'user',
75
+ id: '6b67c24d-5f09-4845-b753-0b73abf658f0',
76
+ name_label: 'debian 12 hub disk',
77
+ href: '/rest/v0/vdi-snapshots/6b67c24d-5f09-4845-b753-0b73abf658f0',
78
+ },
79
+ {
80
+ VDI_type: 'user',
81
+ id: '11d85da8-7caf-4a47-b030-15e78adb3f72',
82
+ name_label: 'xoa root',
83
+ href: '/rest/v0/vdi-snapshots/11d85da8-7caf-4a47-b030-15e78adb3f72',
84
+ },
85
+ ];
@@ -176,3 +176,17 @@ export const vmSnapshot = {
176
176
  suspendVdi: 'string',
177
177
  type: 'VM-snapshot',
178
178
  };
179
+ export const vmSnapshotVdis = [
180
+ {
181
+ VDI_type: 'user',
182
+ id: 'a2831a61-6b1c-41e3-a328-bdfd13e76488',
183
+ name_label: 'Debian Buster 10_oreva',
184
+ href: '/rest/v0/vdi-snapshots/a2831a61-6b1c-41e3-a328-bdfd13e76488',
185
+ },
186
+ {
187
+ VDI_type: 'user',
188
+ id: '41c52deb-0083-471e-bd72-252ea06a48b9',
189
+ name_label: 'MRA XCP 8.2_uvuvo',
190
+ href: '/rest/v0/vdi-snapshots/41c52deb-0083-471e-bd72-252ea06a48b9',
191
+ },
192
+ ];
@@ -89,3 +89,17 @@ export const vmTemplate = {
89
89
  $poolId: 'b7569d99-30f8-178a-7d94-801de3e29b5b',
90
90
  _xapiRef: 'OpaqueRef:3a9b74fe-57d5-52f7-31ec-fbb0de9e8a1e',
91
91
  };
92
+ export const vmTemplateVdis = [
93
+ {
94
+ VDI_type: 'user',
95
+ id: 'cbfeabfc-20c8-46eb-a094-84076eb29f04',
96
+ name_label: 'Hard disk 1',
97
+ href: '/rest/v0/vdis/cbfeabfc-20c8-46eb-a094-84076eb29f04',
98
+ },
99
+ {
100
+ VDI_type: 'user',
101
+ id: 'bee66a2e-68a4-4ff8-9ed4-2f429587524f',
102
+ name_label: 'Hard disk 2',
103
+ href: '/rest/v0/vdis/bee66a2e-68a4-4ff8-9ed4-2f429587524f',
104
+ },
105
+ ];
@@ -243,3 +243,17 @@ export const vmStatsExample = {
243
243
  ],
244
244
  },
245
245
  };
246
+ export const vmVdis = [
247
+ {
248
+ VDI_type: 'user',
249
+ id: '11045407-4764-4c1c-8865-63f89d686b1b',
250
+ name_label: 'Debian Bookworm 12_ogupi',
251
+ href: '/rest/v0/vdis/11045407-4764-4c1c-8865-63f89d686b1b',
252
+ },
253
+ {
254
+ VDI_type: 'user',
255
+ id: '0eb73d40-e5f8-443d-b611-a52e03858a6a',
256
+ name_label: 'MRA TrueNAS_emodi',
257
+ href: '/rest/v0/vdis/0eb73d40-e5f8-443d-b611-a52e03858a6a',
258
+ },
259
+ ];