@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.
@@ -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
- const user = restApi.getCurrentUser();
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
+ };