@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.
@@ -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
+ };