@xen-orchestra/rest-api 0.10.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.
- package/dist/abstract-classes/base-controller.mjs +24 -2
- package/dist/alarms/alarm.controller.mjs +11 -61
- package/dist/alarms/alarm.service.mjs +67 -0
- package/dist/backup-repositories/backup-repositories.controller.mjs +61 -0
- package/dist/groups/group.controller.mjs +15 -2
- package/dist/helpers/object-wrapper.helper.mjs +8 -1
- package/dist/helpers/utils.helper.mjs +74 -1
- package/dist/hosts/host.controller.mjs +87 -3
- package/dist/hosts/host.service.mjs +78 -0
- package/dist/ioc/ioc.mjs +25 -1
- package/dist/messages/message.controller.mjs +2 -2
- package/dist/middlewares/generic-error-handler.middleware.mjs +15 -11
- package/dist/middlewares/tsoa-to-xo-error.middleware.mjs +1 -1
- package/dist/networks/network.controller.mjs +34 -2
- package/dist/open-api/oa-examples/alarm.oa-example.mjs +12 -0
- package/dist/open-api/oa-examples/backup-repository.oa-example.mjs +31 -0
- package/dist/open-api/oa-examples/pool.oa-example.mjs +401 -0
- package/dist/open-api/oa-examples/user.oa-example.mjs +1 -0
- package/dist/open-api/routes/routes.js +708 -113
- package/dist/pifs/pif.controller.mjs +34 -2
- package/dist/pools/pool.controller.mjs +84 -3
- package/dist/pools/pool.service.mjs +212 -0
- package/dist/rest-api/rest-api.mjs +6 -1
- package/dist/srs/sr.controller.mjs +34 -2
- package/dist/users/user.controller.mjs +32 -3
- package/dist/vdi-snapshots/vdi-snapshot.controller.mjs +34 -2
- package/dist/vdis/vdi.controller.mjs +34 -2
- package/dist/vm-templates/vm-template.controller.mjs +34 -2
- package/dist/vms/vm.controller.mjs +1 -1
- package/dist/vms/vm.service.mjs +40 -0
- package/dist/xoa/xoa.service.mjs +96 -110
- package/open-api/spec/swagger.json +6069 -1899
- package/package.json +3 -3
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { Controller } from 'tsoa';
|
|
2
|
+
import { createGzip } from 'node:zlib';
|
|
3
|
+
import { pipeline } from 'node:stream/promises';
|
|
2
4
|
import { Readable } from 'node:stream';
|
|
3
5
|
import { BASE_URL } from '../index.mjs';
|
|
4
6
|
import { makeNdJsonStream } from '../helpers/stream.helper.mjs';
|
|
@@ -11,8 +13,8 @@ export class BaseController extends Controller {
|
|
|
11
13
|
super();
|
|
12
14
|
this.restApi = restApi;
|
|
13
15
|
}
|
|
14
|
-
sendObjects(objects, req) {
|
|
15
|
-
const mapper = makeObjectMapper(req);
|
|
16
|
+
sendObjects(objects, req, path) {
|
|
17
|
+
const mapper = makeObjectMapper(req, path);
|
|
16
18
|
const mappedObjects = objects.map(mapper);
|
|
17
19
|
if (req.query.ndjson === 'true') {
|
|
18
20
|
const res = req.res;
|
|
@@ -49,4 +51,24 @@ export class BaseController extends Controller {
|
|
|
49
51
|
getXapi(maybeId) {
|
|
50
52
|
return this.restApi.xoApp.getXapi(maybeId);
|
|
51
53
|
}
|
|
54
|
+
maybeCompressResponse(req, res) {
|
|
55
|
+
let transform;
|
|
56
|
+
let acceptEncoding = req.headers['accept-encoding'];
|
|
57
|
+
acceptEncoding = Array.isArray(acceptEncoding) ? acceptEncoding : acceptEncoding?.split(',');
|
|
58
|
+
if (acceptEncoding !== undefined &&
|
|
59
|
+
acceptEncoding.some(encoding => {
|
|
60
|
+
const value = encoding.split(';')[0].trim().toLowerCase();
|
|
61
|
+
// support `x-gzip` as an alias for `gzip`
|
|
62
|
+
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Encoding#gzip
|
|
63
|
+
return value === 'gzip' || value === 'x-gzip';
|
|
64
|
+
})) {
|
|
65
|
+
res.setHeader('Content-Encoding', 'gzip');
|
|
66
|
+
transform = createGzip();
|
|
67
|
+
}
|
|
68
|
+
if (transform !== undefined) {
|
|
69
|
+
pipeline(transform, res).catch(noop);
|
|
70
|
+
return transform;
|
|
71
|
+
}
|
|
72
|
+
return res;
|
|
73
|
+
}
|
|
52
74
|
}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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(
|
|
62
|
-
|
|
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 (!
|
|
88
|
-
|
|
36
|
+
if (!this.#alarmService.isAlarm(maybeAlarm)) {
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|
|
@@ -7,16 +7,22 @@ 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 { Example, Get, Path, Query, Request, Response, Route, Security, SuccessResponse, Tags } from 'tsoa';
|
|
11
11
|
import { inject } from 'inversify';
|
|
12
|
+
import { pipeline } from 'node:stream/promises';
|
|
12
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';
|
|
13
17
|
import { host, hostIds, hostStats, partialHosts } from '../open-api/oa-examples/host.oa-example.mjs';
|
|
14
18
|
import { RestApi } from '../rest-api/rest-api.mjs';
|
|
15
19
|
import { XapiXoController } from '../abstract-classes/xapi-xo-controller.mjs';
|
|
16
20
|
import { internalServerErrorResp, notFoundResp, unauthorizedResp, } from '../open-api/common/response.common.mjs';
|
|
17
21
|
let HostController = class HostController extends XapiXoController {
|
|
18
|
-
|
|
22
|
+
#alarmService;
|
|
23
|
+
constructor(restApi, alarmService) {
|
|
19
24
|
super('host', restApi);
|
|
25
|
+
this.#alarmService = alarmService;
|
|
20
26
|
}
|
|
21
27
|
/**
|
|
22
28
|
* @example fields "id,name_label,productBrand"
|
|
@@ -39,6 +45,55 @@ let HostController = class HostController extends XapiXoController {
|
|
|
39
45
|
getHostStats(id, granularity) {
|
|
40
46
|
return this.restApi.xoApp.getXapiHostStats(id, granularity);
|
|
41
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Host must be running
|
|
50
|
+
*
|
|
51
|
+
* Download the audit log of a host.
|
|
52
|
+
*
|
|
53
|
+
* @example id "b61a5c92-700e-4966-a13b-00633f03eea8"
|
|
54
|
+
*
|
|
55
|
+
*/
|
|
56
|
+
async getAuditLog(req, id) {
|
|
57
|
+
const xapiHost = this.getXapiObject(id);
|
|
58
|
+
const res = req.res;
|
|
59
|
+
const response = await xapiHost.$xapi.getResource('/audit_log', { host: xapiHost });
|
|
60
|
+
const date = new Date().toISOString();
|
|
61
|
+
const headers = new Headers({
|
|
62
|
+
'Content-Type': 'application/octet-stream',
|
|
63
|
+
'Content-Disposition': `attachment; filename="${host.name_label}-${date}-audit.txt"`,
|
|
64
|
+
});
|
|
65
|
+
res.setHeaders(headers);
|
|
66
|
+
await pipeline(response.body, this.maybeCompressResponse(req, res));
|
|
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
|
+
}
|
|
42
97
|
};
|
|
43
98
|
__decorate([
|
|
44
99
|
Example(hostIds),
|
|
@@ -65,12 +120,41 @@ __decorate([
|
|
|
65
120
|
__param(0, Path()),
|
|
66
121
|
__param(1, Query())
|
|
67
122
|
], HostController.prototype, "getHostStats", null);
|
|
123
|
+
__decorate([
|
|
124
|
+
Get('{id}/audit.txt'),
|
|
125
|
+
SuccessResponse(200, 'Download started', 'application/octet-stream'),
|
|
126
|
+
Response(notFoundResp.status, notFoundResp.description),
|
|
127
|
+
Response(internalServerErrorResp.status, internalServerErrorResp.description),
|
|
128
|
+
__param(0, Request()),
|
|
129
|
+
__param(1, Path())
|
|
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);
|
|
68
151
|
HostController = __decorate([
|
|
69
152
|
Route('hosts'),
|
|
70
153
|
Security('*'),
|
|
71
154
|
Response(unauthorizedResp.status, unauthorizedResp.description),
|
|
72
155
|
Tags('hosts'),
|
|
73
156
|
provide(HostController),
|
|
74
|
-
__param(0, inject(RestApi))
|
|
157
|
+
__param(0, inject(RestApi)),
|
|
158
|
+
__param(1, inject(AlarmService))
|
|
75
159
|
], HostController);
|
|
76
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.
|
|
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';
|
|
@@ -38,7 +38,7 @@ let MessageController = class MessageController extends XapiXoController {
|
|
|
38
38
|
getObject(id) {
|
|
39
39
|
const message = super.getObject(id);
|
|
40
40
|
if (alarmPredicate(message)) {
|
|
41
|
-
|
|
41
|
+
throw noSuchObject(id, 'message');
|
|
42
42
|
}
|
|
43
43
|
return message;
|
|
44
44
|
}
|