@xen-orchestra/rest-api 0.33.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/README.md +32 -0
- package/dist/backup-jobs/backup-job.service.mjs +4 -0
- package/dist/backup-repositories/backup-repositories.controller.mjs +197 -6
- package/dist/backup-repositories/backup-repository.service.mjs +46 -0
- package/dist/hosts/host.controller.mjs +332 -2
- package/dist/hosts/host.service.mjs +76 -0
- package/dist/ioc/ioc.mjs +8 -0
- package/dist/middlewares/acl.middleware.mjs +10 -1
- package/dist/middlewares/deprecated.middleware.mjs +10 -0
- package/dist/middlewares/generic-error-handler.middleware.mjs +1 -0
- package/dist/open-api/oa-examples/backup-repository.oa-example.mjs +7 -0
- package/dist/open-api/routes/routes.js +488 -20
- package/dist/open-api/schema/build-openapi-schema.mjs +8 -1
- package/dist/pbds/pbd.controller.mjs +10 -0
- package/dist/pools/pool.controller.mjs +48 -0
- package/dist/router/external-router.mjs +4 -1
- package/dist/srs/sr.controller.mjs +15 -0
- package/dist/vbds/vbd.controller.mjs +20 -1
- package/dist/vdi-snapshots/vdi-snapshot.controller.mjs +27 -2
- package/dist/vdis/vdi.controller.mjs +26 -2
- package/dist/vifs/vif.controller.mjs +79 -6
- package/dist/vm-snapshots/vm-snapshot.controller.mjs +2 -1
- package/dist/vm-templates/vm-template.controller.mjs +2 -1
- package/dist/vms/vm.controller.mjs +21 -3
- package/dist/vms/vm.service.mjs +12 -11
- package/open-api/spec/swagger.json +1701 -76
- package/package.json +7 -7
|
@@ -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
|
}
|
package/dist/ioc/ioc.mjs
CHANGED
|
@@ -16,6 +16,7 @@ import { NetworkService } from '../networks/network.service.mjs';
|
|
|
16
16
|
import { BackupArchiveService } from '../backup-archives/backup-archive.service.mjs';
|
|
17
17
|
import { SrService } from '../srs/sr.service.mjs';
|
|
18
18
|
import { LicenseService } from '../licenses/license.service.mjs';
|
|
19
|
+
import { BackupRepositoryService } from '../backup-repositories/backup-repository.service.mjs';
|
|
19
20
|
const iocContainer = new Container();
|
|
20
21
|
export function setupContainer(xoApp) {
|
|
21
22
|
decorate(injectable(), Controller);
|
|
@@ -125,5 +126,12 @@ export function setupContainer(xoApp) {
|
|
|
125
126
|
return new LicenseService(restApi);
|
|
126
127
|
})
|
|
127
128
|
.inSingletonScope();
|
|
129
|
+
iocContainer
|
|
130
|
+
.bind(BackupRepositoryService)
|
|
131
|
+
.toDynamicValue(ctx => {
|
|
132
|
+
const restApi = ctx.container.get(RestApi);
|
|
133
|
+
return new BackupRepositoryService(restApi);
|
|
134
|
+
})
|
|
135
|
+
.inSingletonScope();
|
|
128
136
|
}
|
|
129
137
|
export { iocContainer };
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { getMissingPrivileges, } from '@xen-orchestra/acl';
|
|
2
|
+
import { createLogger } from '@xen-orchestra/log';
|
|
2
3
|
import { RestApi } from '../rest-api/rest-api.mjs';
|
|
3
4
|
import { iocContainer } from '../ioc/ioc.mjs';
|
|
4
5
|
import { ValidateError } from 'tsoa';
|
|
5
6
|
import { ApiError } from '../helpers/error.helper.mjs';
|
|
6
7
|
export const ACL_MIDDLEWARE_NAME = '_aclMiddleware';
|
|
8
|
+
const log = createLogger('xo:rest-api:middleware');
|
|
7
9
|
export function actionsFromBody(actions) {
|
|
8
10
|
return ({ req }) => actions.filter(action => {
|
|
9
11
|
const [, field] = action.split(':');
|
|
@@ -107,7 +109,14 @@ export function acl(acls) {
|
|
|
107
109
|
const _acls = acls.map(normalizeAclEntry);
|
|
108
110
|
async function middleware(req, res, next) {
|
|
109
111
|
const restApi = iocContainer.get(RestApi);
|
|
110
|
-
|
|
112
|
+
let user;
|
|
113
|
+
try {
|
|
114
|
+
user = restApi.getCurrentUser();
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
log.warn('An unauthenticated user made its way to acl middleware', error);
|
|
118
|
+
return next(error);
|
|
119
|
+
}
|
|
111
120
|
const invalidFields = {};
|
|
112
121
|
const missingPrivilegeParams = [];
|
|
113
122
|
for (const acl of _acls) {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createLogger } from '@xen-orchestra/log';
|
|
2
|
+
const log = createLogger('xo:rest-api:deprecated.middleware');
|
|
3
|
+
export function vmExportCompressDeprecated(req, _res, next) {
|
|
4
|
+
const compress = req.query.compress;
|
|
5
|
+
if (compress === 'true' || compress === 'false') {
|
|
6
|
+
log.warn("the query param 'compress' as boolean is deprecated. Please use an explicit value next time");
|
|
7
|
+
req.query.compress = compress === 'true' ? 'gzip' : undefined;
|
|
8
|
+
}
|
|
9
|
+
next();
|
|
10
|
+
}
|
|
@@ -29,6 +29,7 @@ export default function genericErrorHandler(error, req, res, _next) {
|
|
|
29
29
|
}
|
|
30
30
|
else if (invalidCredentials.is(error)) {
|
|
31
31
|
statusCode = 401;
|
|
32
|
+
res.setHeader('WWW-Authenticate', 'Basic realm="xo"');
|
|
32
33
|
}
|
|
33
34
|
else if (objectAlreadyExists.is(error)) {
|
|
34
35
|
statusCode = 409;
|
|
@@ -29,3 +29,10 @@ export const backupRepository = {
|
|
|
29
29
|
id: '677e50c5-8d8a-4c89-b1ac-e2f4593d0ebb',
|
|
30
30
|
url: 's3://FOIS5DY532RGXD62TJ52:obfuscated-q3oi6d9X8uenGvdLnHk2@s3.us-east-2.amazonaws.com/with-lock/backup?useVhdDirectory=true',
|
|
31
31
|
};
|
|
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
|
+
};
|