@xen-orchestra/rest-api 0.34.0 → 0.35.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/backup-repositories/backup-repositories.controller.mjs +84 -2
- package/dist/hosts/host.controller.mjs +332 -2
- package/dist/hosts/host.service.mjs +76 -0
- package/dist/open-api/oa-examples/backup-repository.oa-example.mjs +6 -0
- package/dist/open-api/routes/routes.js +307 -4
- package/dist/open-api/schema/build-openapi-schema.mjs +8 -1
- package/dist/router/external-router.mjs +4 -1
- package/open-api/spec/swagger.json +1751 -795
- package/package.json +4 -4
|
@@ -13,12 +13,13 @@ import { provide } from 'inversify-binding-decorators';
|
|
|
13
13
|
import { json } from 'express';
|
|
14
14
|
import { forbiddenOperation } from 'xo-common/api-errors.js';
|
|
15
15
|
import { acl, actionsFromBody } from '../middlewares/acl.middleware.mjs';
|
|
16
|
-
import { badRequestResp, createdResp, forbiddenOperationResp, invalidParameters, noContentResp, notFoundResp, unauthorizedResp, } from '../open-api/common/response.common.mjs';
|
|
17
|
-
import { backupRepositoryIds, partialBackupRepositories, backupRepository, backupRepositoryId, } from '../open-api/oa-examples/backup-repository.oa-example.mjs';
|
|
16
|
+
import { asynchronousActionResp, badRequestResp, createdResp, forbiddenOperationResp, invalidParameters, noContentResp, internalServerErrorResp, notFoundResp, unauthorizedResp, } from '../open-api/common/response.common.mjs';
|
|
17
|
+
import { backupRepositoryIds, partialBackupRepositories, backupRepository, backupRepositoryId, backupRepositoryBenchmark, backupRepositoryHeath, } from '../open-api/oa-examples/backup-repository.oa-example.mjs';
|
|
18
18
|
import { XoController } from '../abstract-classes/xo-controller.mjs';
|
|
19
19
|
import { RestApi } from '../rest-api/rest-api.mjs';
|
|
20
20
|
import { BackupRepositoryService } from './backup-repository.service.mjs';
|
|
21
21
|
import { taskLocation } from '../open-api/oa-examples/task.oa-example.mjs';
|
|
22
|
+
import { ApiError } from '../helpers/error.helper.mjs';
|
|
22
23
|
let BackupRepositoryController = class BackupRepositoryController extends XoController {
|
|
23
24
|
#backupRepositoryService;
|
|
24
25
|
constructor(restApi, backupRepositoryService) {
|
|
@@ -107,6 +108,51 @@ let BackupRepositoryController = class BackupRepositoryController extends XoCont
|
|
|
107
108
|
async updateBackupRepository(id, body) {
|
|
108
109
|
await this.restApi.xoApp.updateRemote(id, body);
|
|
109
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* Pings the backup-repository to check its health
|
|
113
|
+
*
|
|
114
|
+
* Required privilege:
|
|
115
|
+
* - resource: backup-repository, action: read
|
|
116
|
+
*
|
|
117
|
+
* @example id "c4284e12-37c9-7967-b9e8-83ef229c3e03"
|
|
118
|
+
*/
|
|
119
|
+
async getBackupRepositoryHealth(id) {
|
|
120
|
+
return this.restApi.xoApp.pingRemote(id);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Runs a benchmark for write and read speed on the Backup-repository.
|
|
124
|
+
* Saves the benchmark result on the BR and returns the results, speeds are in bytes/sec.
|
|
125
|
+
* 502 if the BR cannot be reached, 400 if there was a problem during the benchmark with the error.
|
|
126
|
+
*
|
|
127
|
+
* Required privilege:
|
|
128
|
+
* - resource: backup-repository, action: benchmark
|
|
129
|
+
*
|
|
130
|
+
* @example id "c4284e12-37c9-7967-b9e8-83ef229c3e03"
|
|
131
|
+
*/
|
|
132
|
+
benchmarkBackupRepository(id, sync) {
|
|
133
|
+
const backupRepositoryId = id;
|
|
134
|
+
const action = async () => {
|
|
135
|
+
let result;
|
|
136
|
+
try {
|
|
137
|
+
result = await this.restApi.xoApp.testRemote(backupRepositoryId);
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
throw new ApiError('Backup repository unreachable', 502);
|
|
141
|
+
}
|
|
142
|
+
if (!result.success) {
|
|
143
|
+
throw new ApiError('Benchmark failed', 400, { data: result });
|
|
144
|
+
}
|
|
145
|
+
return result;
|
|
146
|
+
};
|
|
147
|
+
return this.createAction(action, {
|
|
148
|
+
sync,
|
|
149
|
+
statusCode: 200,
|
|
150
|
+
taskProperties: {
|
|
151
|
+
name: 'benchmark backup repository',
|
|
152
|
+
objectId: backupRepositoryId,
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
}
|
|
110
156
|
};
|
|
111
157
|
__decorate([
|
|
112
158
|
Example(backupRepositoryIds),
|
|
@@ -185,6 +231,42 @@ __decorate([
|
|
|
185
231
|
__param(0, Path()),
|
|
186
232
|
__param(1, Body())
|
|
187
233
|
], BackupRepositoryController.prototype, "updateBackupRepository", null);
|
|
234
|
+
__decorate([
|
|
235
|
+
Example(backupRepositoryHeath),
|
|
236
|
+
Extension('x-mcp-exposure', 'allow'),
|
|
237
|
+
Get('{id}/health'),
|
|
238
|
+
Middlewares(acl({
|
|
239
|
+
resource: 'backup-repository',
|
|
240
|
+
action: 'read',
|
|
241
|
+
objectId: 'params.id',
|
|
242
|
+
getObject: ({ restApi }) => restApi.xoApp.getRemote,
|
|
243
|
+
})),
|
|
244
|
+
SuccessResponse(200, 'OK'),
|
|
245
|
+
Response(forbiddenOperationResp.status, forbiddenOperationResp.description),
|
|
246
|
+
Response(internalServerErrorResp.status, internalServerErrorResp.description),
|
|
247
|
+
__param(0, Path())
|
|
248
|
+
], BackupRepositoryController.prototype, "getBackupRepositoryHealth", null);
|
|
249
|
+
__decorate([
|
|
250
|
+
Example(taskLocation),
|
|
251
|
+
Example(backupRepositoryBenchmark),
|
|
252
|
+
Extension('x-mcp-exposure', 'confirm'),
|
|
253
|
+
Post('{id}/actions/benchmark'),
|
|
254
|
+
Middlewares(acl({
|
|
255
|
+
resource: 'backup-repository',
|
|
256
|
+
action: 'benchmark',
|
|
257
|
+
objectId: 'params.id',
|
|
258
|
+
getObject: ({ restApi }) => restApi.xoApp.getRemote,
|
|
259
|
+
})),
|
|
260
|
+
SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description),
|
|
261
|
+
Response(200, 'Ok'),
|
|
262
|
+
Response(400, 'Benchmark failed'),
|
|
263
|
+
Response(notFoundResp.status, notFoundResp.description),
|
|
264
|
+
Response(forbiddenOperationResp.status, forbiddenOperationResp.description),
|
|
265
|
+
Response(internalServerErrorResp.status, internalServerErrorResp.description),
|
|
266
|
+
Response(502, 'Backup repository unreachable'),
|
|
267
|
+
__param(0, Path()),
|
|
268
|
+
__param(1, Query())
|
|
269
|
+
], BackupRepositoryController.prototype, "benchmarkBackupRepository", null);
|
|
188
270
|
BackupRepositoryController = __decorate([
|
|
189
271
|
Route('backup-repositories'),
|
|
190
272
|
Security('*'),
|
|
@@ -8,11 +8,12 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
|
8
8
|
return function (target, key) { decorator(target, key, paramIndex); }
|
|
9
9
|
};
|
|
10
10
|
import { Body, Delete, Example, Extension, Get, Middlewares, Path, Post, Put, Query, Request, Response, Route, Security, SuccessResponse, Tags, } from 'tsoa';
|
|
11
|
+
import { HOST_POWER_STATE } from '@vates/types';
|
|
11
12
|
import { asyncEach } from '@vates/async-each';
|
|
12
13
|
import { defer } from 'golike-defer';
|
|
13
14
|
import { json } from 'express';
|
|
14
15
|
import { inject } from 'inversify';
|
|
15
|
-
import { invalidParameters } from 'xo-common/api-errors.js';
|
|
16
|
+
import { incorrectState, invalidParameters } from 'xo-common/api-errors.js';
|
|
16
17
|
import { pipeline } from 'node:stream/promises';
|
|
17
18
|
import { provide } from 'inversify-binding-decorators';
|
|
18
19
|
import { acl } from '../middlewares/acl.middleware.mjs';
|
|
@@ -22,7 +23,7 @@ import { genericAlarmsExample } from '../open-api/oa-examples/alarm.oa-example.m
|
|
|
22
23
|
import { host, hostIds, hostSmt, hostMissingPatches, hostStats, partialHosts, } from '../open-api/oa-examples/host.oa-example.mjs';
|
|
23
24
|
import { RestApi } from '../rest-api/rest-api.mjs';
|
|
24
25
|
import { XapiXoController } from '../abstract-classes/xapi-xo-controller.mjs';
|
|
25
|
-
import { asynchronousActionResp, badRequestResp, featureUnauthorized, forbiddenOperationResp, internalServerErrorResp, invalidParameters as invalidParametersResp, noContentResp, notFoundResp, unauthorizedResp, } from '../open-api/common/response.common.mjs';
|
|
26
|
+
import { asynchronousActionResp, badRequestResp, featureUnauthorized, forbiddenOperationResp, internalServerErrorResp, invalidParameters as invalidParametersResp, incorrectStateResp, noContentResp, notFoundResp, unauthorizedResp, } from '../open-api/common/response.common.mjs';
|
|
26
27
|
import { HostService } from './host.service.mjs';
|
|
27
28
|
import { messageIds, partialMessages } from '../open-api/oa-examples/message.oa-example.mjs';
|
|
28
29
|
import { partialTasks, taskIds, taskLocation } from '../open-api/oa-examples/task.oa-example.mjs';
|
|
@@ -313,6 +314,225 @@ let HostController = class HostController extends XapiXoController {
|
|
|
313
314
|
},
|
|
314
315
|
});
|
|
315
316
|
}
|
|
317
|
+
/**
|
|
318
|
+
* Required privilege:
|
|
319
|
+
* - resource: host, action: start
|
|
320
|
+
*
|
|
321
|
+
* Start a host.
|
|
322
|
+
*
|
|
323
|
+
* @example id "b61a5c92-700e-4966-a13b-00633f03eea8"
|
|
324
|
+
*/
|
|
325
|
+
startHost(id, sync) {
|
|
326
|
+
const hostId = id;
|
|
327
|
+
const action = async () => {
|
|
328
|
+
await this.getXapiObject(hostId).$xapi.powerOnHost(hostId);
|
|
329
|
+
};
|
|
330
|
+
return this.createAction(action, {
|
|
331
|
+
sync,
|
|
332
|
+
statusCode: noContentResp.status,
|
|
333
|
+
taskProperties: {
|
|
334
|
+
name: 'start host',
|
|
335
|
+
objectId: hostId,
|
|
336
|
+
},
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Required privilege:
|
|
341
|
+
* - resource: host, action: shutdown:clean
|
|
342
|
+
*
|
|
343
|
+
* Shutdown a host.
|
|
344
|
+
*
|
|
345
|
+
* @example id "b61a5c92-700e-4966-a13b-00633f03eea8"
|
|
346
|
+
* @example body { "bypassBackupCheck": false, "bypassEvacuate": false }
|
|
347
|
+
*/
|
|
348
|
+
cleanShutdownHost(id, body, sync) {
|
|
349
|
+
const hostId = id;
|
|
350
|
+
const action = async () => {
|
|
351
|
+
await this.#hostService.cleanShutdownHost(hostId, body);
|
|
352
|
+
};
|
|
353
|
+
return this.createAction(action, {
|
|
354
|
+
sync,
|
|
355
|
+
statusCode: noContentResp.status,
|
|
356
|
+
taskProperties: {
|
|
357
|
+
name: 'clean shutdown host',
|
|
358
|
+
objectId: hostId,
|
|
359
|
+
params: body,
|
|
360
|
+
},
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Required privilege:
|
|
365
|
+
* - resource: host, action: reboot:clean
|
|
366
|
+
*
|
|
367
|
+
* Reboot a host by evacuating its VMs to other hosts first.
|
|
368
|
+
*
|
|
369
|
+
* Checks for active backup jobs and version compatibility before rebooting.
|
|
370
|
+
*
|
|
371
|
+
* @example id "b61a5c92-700e-4966-a13b-00633f03eea8"
|
|
372
|
+
* @example body {
|
|
373
|
+
* "force": false,
|
|
374
|
+
* "bypassBackupCheck": false,
|
|
375
|
+
* "bypassVersionCheck": false
|
|
376
|
+
* }
|
|
377
|
+
*/
|
|
378
|
+
cleanRebootHost(id, body, sync) {
|
|
379
|
+
const force = body?.force ?? false;
|
|
380
|
+
const opts = {
|
|
381
|
+
force,
|
|
382
|
+
bypassBackupCheck: body?.bypassBackupCheck ?? force,
|
|
383
|
+
bypassVersionCheck: body?.bypassVersionCheck ?? force,
|
|
384
|
+
};
|
|
385
|
+
const hostId = id;
|
|
386
|
+
const action = async () => {
|
|
387
|
+
await this.#hostService.cleanRebootHost(hostId, opts);
|
|
388
|
+
};
|
|
389
|
+
return this.createAction(action, {
|
|
390
|
+
sync,
|
|
391
|
+
statusCode: noContentResp.status,
|
|
392
|
+
taskProperties: {
|
|
393
|
+
name: 'clean reboot host',
|
|
394
|
+
objectId: hostId,
|
|
395
|
+
params: body,
|
|
396
|
+
},
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Required privilege:
|
|
401
|
+
* - resource: host, action: reboot:smart
|
|
402
|
+
*
|
|
403
|
+
* Reboot a host by suspending its VMs in place.
|
|
404
|
+
*
|
|
405
|
+
* @example id "b61a5c92-700e-4966-a13b-00633f03eea8"
|
|
406
|
+
* @example body {
|
|
407
|
+
* "bypassBackupCheck": false,
|
|
408
|
+
* "bypassVersionCheck": false,
|
|
409
|
+
* "bypassBlockedSuspend": false,
|
|
410
|
+
* "bypassCurrentVmCheck": false
|
|
411
|
+
* }
|
|
412
|
+
*/
|
|
413
|
+
smartRebootHost(id, body, sync) {
|
|
414
|
+
const opts = {
|
|
415
|
+
bypassBackupCheck: body?.bypassBackupCheck ?? false,
|
|
416
|
+
bypassVersionCheck: body?.bypassVersionCheck ?? false,
|
|
417
|
+
bypassBlockedSuspend: body?.bypassBlockedSuspend ?? false,
|
|
418
|
+
bypassCurrentVmCheck: body?.bypassCurrentVmCheck ?? false,
|
|
419
|
+
};
|
|
420
|
+
const hostId = id;
|
|
421
|
+
const action = async () => {
|
|
422
|
+
await this.#hostService.smartRebootHost(hostId, opts);
|
|
423
|
+
};
|
|
424
|
+
return this.createAction(action, {
|
|
425
|
+
sync,
|
|
426
|
+
statusCode: noContentResp.status,
|
|
427
|
+
taskProperties: {
|
|
428
|
+
name: 'smart reboot host',
|
|
429
|
+
objectId: hostId,
|
|
430
|
+
params: body,
|
|
431
|
+
},
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Required privilege:
|
|
436
|
+
* - resource: host, action: restart-toolstack
|
|
437
|
+
*
|
|
438
|
+
* Restart a host's toolstack.
|
|
439
|
+
*
|
|
440
|
+
* @example id "b61a5c92-700e-4966-a13b-00633f03eea8"
|
|
441
|
+
*/
|
|
442
|
+
restartHostToolstack(id, body, sync) {
|
|
443
|
+
const hostId = id;
|
|
444
|
+
const action = async () => {
|
|
445
|
+
await this.#hostService.restartToolstack(hostId, body);
|
|
446
|
+
};
|
|
447
|
+
return this.createAction(action, {
|
|
448
|
+
sync,
|
|
449
|
+
statusCode: noContentResp.status,
|
|
450
|
+
taskProperties: {
|
|
451
|
+
name: "restart host's toolstack",
|
|
452
|
+
objectId: hostId,
|
|
453
|
+
params: body,
|
|
454
|
+
},
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Required privilege:
|
|
459
|
+
* - resource: host, action: shutdown:emergency
|
|
460
|
+
*
|
|
461
|
+
* Shut down a host by disabling it, suspending its VMs in place, and powering off without migrating them.
|
|
462
|
+
*
|
|
463
|
+
* Unlike `clean_shutdown`, VMs are not evacuated to other hosts, they are suspended
|
|
464
|
+
* on the same host (errors are ignored) before the host shuts down.
|
|
465
|
+
* No backup check is performed.
|
|
466
|
+
*
|
|
467
|
+
* @example id "b61a5c92-700e-4966-a13b-00633f03eea8"
|
|
468
|
+
*/
|
|
469
|
+
emergencyShutdownHost(id, sync) {
|
|
470
|
+
const hostId = id;
|
|
471
|
+
const action = async () => {
|
|
472
|
+
await this.getXapiObject(hostId).$xapi.emergencyShutdownHost(hostId);
|
|
473
|
+
};
|
|
474
|
+
return this.createAction(action, {
|
|
475
|
+
sync,
|
|
476
|
+
statusCode: noContentResp.status,
|
|
477
|
+
taskProperties: {
|
|
478
|
+
name: 'emergency shutdown host',
|
|
479
|
+
objectId: hostId,
|
|
480
|
+
},
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Required privilege:
|
|
485
|
+
* - resource: host, action: detach
|
|
486
|
+
*
|
|
487
|
+
* Detaches a host from its pool.
|
|
488
|
+
*
|
|
489
|
+
* @example id "b61a5c92-700e-4966-a13b-00633f03eea8"
|
|
490
|
+
*/
|
|
491
|
+
detachHost(id, sync) {
|
|
492
|
+
const hostId = id;
|
|
493
|
+
const action = async () => {
|
|
494
|
+
await this.restApi.xoApp.detachHostFromPool(hostId);
|
|
495
|
+
};
|
|
496
|
+
return this.createAction(action, {
|
|
497
|
+
sync,
|
|
498
|
+
statusCode: noContentResp.status,
|
|
499
|
+
taskProperties: {
|
|
500
|
+
name: 'detach host',
|
|
501
|
+
objectId: hostId,
|
|
502
|
+
},
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Required privilege:
|
|
507
|
+
* - resource: host, action: forget
|
|
508
|
+
*
|
|
509
|
+
* Forgets a host, host must not be running.
|
|
510
|
+
*
|
|
511
|
+
* @example id "b61a5c92-700e-4966-a13b-00633f03eea8"
|
|
512
|
+
*/
|
|
513
|
+
forgetHost(id, sync) {
|
|
514
|
+
const hostId = id;
|
|
515
|
+
const action = async () => {
|
|
516
|
+
const host = this.getObject(hostId);
|
|
517
|
+
if (host.power_state === HOST_POWER_STATE.RUNNING) {
|
|
518
|
+
throw incorrectState({
|
|
519
|
+
actual: host.power_state,
|
|
520
|
+
expected: HOST_POWER_STATE.HALTED,
|
|
521
|
+
object: host.id,
|
|
522
|
+
property: 'power_state',
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
await this.getXapiObject(hostId).$xapi.forgetHost(hostId);
|
|
526
|
+
};
|
|
527
|
+
return this.createAction(action, {
|
|
528
|
+
sync,
|
|
529
|
+
statusCode: noContentResp.status,
|
|
530
|
+
taskProperties: {
|
|
531
|
+
name: 'forget host',
|
|
532
|
+
objectId: hostId,
|
|
533
|
+
},
|
|
534
|
+
});
|
|
535
|
+
}
|
|
316
536
|
};
|
|
317
537
|
__decorate([
|
|
318
538
|
Example(hostIds),
|
|
@@ -514,6 +734,116 @@ __decorate([
|
|
|
514
734
|
__param(0, Path()),
|
|
515
735
|
__param(1, Query())
|
|
516
736
|
], HostController.prototype, "enable", null);
|
|
737
|
+
__decorate([
|
|
738
|
+
Example(taskLocation),
|
|
739
|
+
Extension('x-mcp-exposure', 'confirm'),
|
|
740
|
+
Post('{id}/actions/start'),
|
|
741
|
+
Middlewares(acl({ resource: 'host', action: 'start', objectId: 'params.id' })),
|
|
742
|
+
SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description),
|
|
743
|
+
Response(noContentResp.status, noContentResp.description),
|
|
744
|
+
Response(forbiddenOperationResp.status, forbiddenOperationResp.description),
|
|
745
|
+
Response(notFoundResp.status, notFoundResp.description),
|
|
746
|
+
Response(internalServerErrorResp.status, internalServerErrorResp.description),
|
|
747
|
+
__param(0, Path()),
|
|
748
|
+
__param(1, Query())
|
|
749
|
+
], HostController.prototype, "startHost", null);
|
|
750
|
+
__decorate([
|
|
751
|
+
Example(taskLocation),
|
|
752
|
+
Extension('x-mcp-exposure', 'deny'),
|
|
753
|
+
Post('{id}/actions/clean_shutdown'),
|
|
754
|
+
Middlewares([json(), acl({ resource: 'host', action: 'shutdown:clean', objectId: 'params.id' })]),
|
|
755
|
+
SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description),
|
|
756
|
+
Response(noContentResp.status, noContentResp.description),
|
|
757
|
+
Response(forbiddenOperationResp.status, forbiddenOperationResp.description),
|
|
758
|
+
Response(notFoundResp.status, notFoundResp.description),
|
|
759
|
+
Response(internalServerErrorResp.status, internalServerErrorResp.description),
|
|
760
|
+
__param(0, Path()),
|
|
761
|
+
__param(1, Body()),
|
|
762
|
+
__param(2, Query())
|
|
763
|
+
], HostController.prototype, "cleanShutdownHost", null);
|
|
764
|
+
__decorate([
|
|
765
|
+
Example(taskLocation),
|
|
766
|
+
Extension('x-mcp-exposure', 'deny'),
|
|
767
|
+
Post('{id}/actions/clean_reboot'),
|
|
768
|
+
Middlewares([json(), acl({ resource: 'host', action: 'reboot:clean', objectId: 'params.id' })]),
|
|
769
|
+
SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description),
|
|
770
|
+
Response(noContentResp.status, noContentResp.description),
|
|
771
|
+
Response(forbiddenOperationResp.status, forbiddenOperationResp.description),
|
|
772
|
+
Response(notFoundResp.status, notFoundResp.description),
|
|
773
|
+
Response(internalServerErrorResp.status, internalServerErrorResp.description),
|
|
774
|
+
__param(0, Path()),
|
|
775
|
+
__param(1, Body()),
|
|
776
|
+
__param(2, Query())
|
|
777
|
+
], HostController.prototype, "cleanRebootHost", null);
|
|
778
|
+
__decorate([
|
|
779
|
+
Example(taskLocation),
|
|
780
|
+
Extension('x-mcp-exposure', 'deny'),
|
|
781
|
+
Post('{id}/actions/smart_reboot'),
|
|
782
|
+
Middlewares([json(), acl({ resource: 'host', action: 'reboot:smart', objectId: 'params.id' })]),
|
|
783
|
+
SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description),
|
|
784
|
+
Response(noContentResp.status, noContentResp.description),
|
|
785
|
+
Response(forbiddenOperationResp.status, forbiddenOperationResp.description),
|
|
786
|
+
Response(featureUnauthorized.status, featureUnauthorized.description),
|
|
787
|
+
Response(notFoundResp.status, notFoundResp.description),
|
|
788
|
+
Response(internalServerErrorResp.status, internalServerErrorResp.description),
|
|
789
|
+
__param(0, Path()),
|
|
790
|
+
__param(1, Body()),
|
|
791
|
+
__param(2, Query())
|
|
792
|
+
], HostController.prototype, "smartRebootHost", null);
|
|
793
|
+
__decorate([
|
|
794
|
+
Example(taskLocation),
|
|
795
|
+
Extension('x-mcp-exposure', 'confirm'),
|
|
796
|
+
Post('{id}/actions/restart_toolstack'),
|
|
797
|
+
Middlewares([json(), acl({ resource: 'host', action: 'restart-toolstack', objectId: 'params.id' })]),
|
|
798
|
+
SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description),
|
|
799
|
+
Response(noContentResp.status, noContentResp.description),
|
|
800
|
+
Response(forbiddenOperationResp.status, forbiddenOperationResp.description),
|
|
801
|
+
Response(notFoundResp.status, notFoundResp.description),
|
|
802
|
+
Response(internalServerErrorResp.status, internalServerErrorResp.description),
|
|
803
|
+
__param(0, Path()),
|
|
804
|
+
__param(1, Body()),
|
|
805
|
+
__param(2, Query())
|
|
806
|
+
], HostController.prototype, "restartHostToolstack", null);
|
|
807
|
+
__decorate([
|
|
808
|
+
Example(taskLocation),
|
|
809
|
+
Extension('x-mcp-exposure', 'deny'),
|
|
810
|
+
Post('{id}/actions/emergency_shutdown'),
|
|
811
|
+
Middlewares(acl({ resource: 'host', action: 'shutdown:emergency', objectId: 'params.id' })),
|
|
812
|
+
SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description),
|
|
813
|
+
Response(noContentResp.status, noContentResp.description),
|
|
814
|
+
Response(forbiddenOperationResp.status, forbiddenOperationResp.description),
|
|
815
|
+
Response(notFoundResp.status, notFoundResp.description),
|
|
816
|
+
Response(internalServerErrorResp.status, internalServerErrorResp.description),
|
|
817
|
+
__param(0, Path()),
|
|
818
|
+
__param(1, Query())
|
|
819
|
+
], HostController.prototype, "emergencyShutdownHost", null);
|
|
820
|
+
__decorate([
|
|
821
|
+
Example(taskLocation),
|
|
822
|
+
Extension('x-mcp-exposure', 'deny'),
|
|
823
|
+
Post('{id}/actions/detach'),
|
|
824
|
+
Middlewares(acl({ resource: 'host', action: 'detach', objectId: 'params.id' })),
|
|
825
|
+
SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description),
|
|
826
|
+
Response(noContentResp.status, noContentResp.description),
|
|
827
|
+
Response(forbiddenOperationResp.status, forbiddenOperationResp.description),
|
|
828
|
+
Response(notFoundResp.status, notFoundResp.description),
|
|
829
|
+
Response(internalServerErrorResp.status, internalServerErrorResp.description),
|
|
830
|
+
__param(0, Path()),
|
|
831
|
+
__param(1, Query())
|
|
832
|
+
], HostController.prototype, "detachHost", null);
|
|
833
|
+
__decorate([
|
|
834
|
+
Example(taskLocation),
|
|
835
|
+
Extension('x-mcp-exposure', 'deny'),
|
|
836
|
+
Post('{id}/actions/forget'),
|
|
837
|
+
Middlewares(acl({ resource: 'host', action: 'forget', objectId: 'params.id' })),
|
|
838
|
+
SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description),
|
|
839
|
+
Response(noContentResp.status, noContentResp.description),
|
|
840
|
+
Response(forbiddenOperationResp.status, forbiddenOperationResp.description),
|
|
841
|
+
Response(notFoundResp.status, notFoundResp.description),
|
|
842
|
+
Response(incorrectStateResp.status, incorrectStateResp.description),
|
|
843
|
+
Response(internalServerErrorResp.status, internalServerErrorResp.description),
|
|
844
|
+
__param(0, Path()),
|
|
845
|
+
__param(1, Query())
|
|
846
|
+
], HostController.prototype, "forgetHost", null);
|
|
517
847
|
HostController = __decorate([
|
|
518
848
|
Route('hosts'),
|
|
519
849
|
Security('*'),
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { asyncEach } from '@vates/async-each';
|
|
2
2
|
import { createLogger } from '@xen-orchestra/log';
|
|
3
3
|
import { HOST_POWER_STATE } from '@vates/types';
|
|
4
|
+
import { incorrectState } from 'xo-common/api-errors.js';
|
|
5
|
+
import semver from 'semver';
|
|
4
6
|
const log = createLogger('xo:rest-api:host-service');
|
|
5
7
|
export class HostService {
|
|
6
8
|
#restApi;
|
|
@@ -74,4 +76,78 @@ export class HostService {
|
|
|
74
76
|
nPoolsWithMissingPatches: poolsWithMissingPatches.size,
|
|
75
77
|
};
|
|
76
78
|
}
|
|
79
|
+
async cleanShutdownHost(hostId, opts = {}) {
|
|
80
|
+
const host = this.#restApi.getObject(hostId);
|
|
81
|
+
if (opts?.bypassBackupCheck) {
|
|
82
|
+
log.warn('host clean_shutdown called with argument "bypassBackupCheck" set to true', { hostId });
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
await this.#restApi.xoApp.backupGuard(host.$pool);
|
|
86
|
+
}
|
|
87
|
+
await this.#restApi.getXapiObject(hostId, 'host').$xapi.shutdownHost(hostId, opts);
|
|
88
|
+
}
|
|
89
|
+
async cleanRebootHost(hostId, opts) {
|
|
90
|
+
const { xapi } = await this.#rebootChecks(hostId, opts);
|
|
91
|
+
await xapi.rebootHost(hostId, opts.force);
|
|
92
|
+
}
|
|
93
|
+
async smartRebootHost(hostId, opts) {
|
|
94
|
+
await this.#restApi.xoApp.checkFeatureAuthorization('SMART_REBOOT');
|
|
95
|
+
const { xapi, xapiHost } = await this.#rebootChecks(hostId, opts);
|
|
96
|
+
await xapi.host_smartReboot(xapiHost.$ref, opts.bypassBlockedSuspend, opts.bypassCurrentVmCheck);
|
|
97
|
+
}
|
|
98
|
+
async restartToolstack(hostId, opts = {}) {
|
|
99
|
+
const host = this.#restApi.getObject(hostId);
|
|
100
|
+
if (opts?.bypassBackupCheck) {
|
|
101
|
+
log.warn('host.restartAgent called with argument "bypassBackupCheck" set to true', { hostId });
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
await this.#restApi.xoApp.backupGuard(host.$pool);
|
|
105
|
+
}
|
|
106
|
+
await this.#restApi.getXapiObject(hostId, 'host').$restartAgent();
|
|
107
|
+
}
|
|
108
|
+
async #rebootChecks(hostId, opts) {
|
|
109
|
+
const host = this.#restApi.getObject(hostId);
|
|
110
|
+
const xapiHost = this.#restApi.getXapiObject(hostId, 'host');
|
|
111
|
+
const poolId = host.$pool;
|
|
112
|
+
const xapi = xapiHost.$xapi;
|
|
113
|
+
if (opts.bypassBackupCheck) {
|
|
114
|
+
log.warn('host.reboot called with "bypassBackupCheck" set to true', { hostId });
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
await this.#restApi.xoApp.backupGuard(poolId);
|
|
118
|
+
}
|
|
119
|
+
if (opts.bypassVersionCheck) {
|
|
120
|
+
log.warn('host.reboot called with "bypassVersionCheck" set to true', { hostId });
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
const pool = this.#restApi.getObject(poolId, 'pool');
|
|
124
|
+
const master = this.#restApi.getObject(pool.master, 'host');
|
|
125
|
+
if (host.rebootRequired && host.id !== master.id) {
|
|
126
|
+
const throwError = () => incorrectState({
|
|
127
|
+
actual: host.rebootRequired,
|
|
128
|
+
expected: false,
|
|
129
|
+
object: master.id,
|
|
130
|
+
property: 'rebootRequired',
|
|
131
|
+
});
|
|
132
|
+
if (semver.lt(master.version, host.version)) {
|
|
133
|
+
log.error(`master version (${master.version}) is older than the host version (${host.version})`, {
|
|
134
|
+
masterId: master.id,
|
|
135
|
+
hostId,
|
|
136
|
+
});
|
|
137
|
+
throwError();
|
|
138
|
+
}
|
|
139
|
+
else if (semver.eq(master.version, host.version)) {
|
|
140
|
+
if ((await xapi.listMissingPatches(master.id)).length > 0) {
|
|
141
|
+
log.error('master has missing patches', { masterId: master.id });
|
|
142
|
+
throwError();
|
|
143
|
+
}
|
|
144
|
+
if (master.rebootRequired) {
|
|
145
|
+
log.error('master needs to reboot', { masterId: master.id });
|
|
146
|
+
throwError();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return { xapi, xapiHost };
|
|
152
|
+
}
|
|
77
153
|
}
|
|
@@ -30,3 +30,9 @@ export const backupRepository = {
|
|
|
30
30
|
url: 's3://FOIS5DY532RGXD62TJ52:obfuscated-q3oi6d9X8uenGvdLnHk2@s3.us-east-2.amazonaws.com/with-lock/backup?useVhdDirectory=true',
|
|
31
31
|
};
|
|
32
32
|
export const backupRepositoryId = { id: '677e50c5-8d8a-4c89-b1ac-e2f4593d0ebb' };
|
|
33
|
+
export const backupRepositoryHeath = { success: true };
|
|
34
|
+
export const backupRepositoryBenchmark = {
|
|
35
|
+
success: true,
|
|
36
|
+
readRate: 7999965,
|
|
37
|
+
writeRate: 7767798,
|
|
38
|
+
};
|