mirror of
https://github.com/vatesfr/xen-orchestra.git
synced 2026-09-10 22:14:48 -05:00
feat(rest-api): backup-repository health/bench routes (#9847)
This commit is contained in:
@@ -312,6 +312,7 @@ export type XoApp = {
|
||||
backupRepositoryIds: XoBackupRepository['id'][],
|
||||
opts?: { _forceRefresh?: boolean; vmId: XoVm['id'] }
|
||||
): Promise<Record<XoBackupRepository['id'], Record<XoVm['id'], XoVmBackupArchive[]>>>
|
||||
pingRemote(id: XoBackupRepository['id']): Promise<{ success: true }>
|
||||
/** Allow to add a new server in the DB (XCP-ng/XenServer) */
|
||||
registerXenServer(
|
||||
body: Pick<XoServer, 'host' | 'httpProxy' | 'label' | 'username'> & {
|
||||
@@ -327,6 +328,12 @@ export type XoApp = {
|
||||
removeUserFromGroup(userId: XoUser['id'], id: XoGroup['id']): Promise<void>
|
||||
runJob(job: AnyXoJob, schedule: XoSchedule): void
|
||||
runWithApiContext: (user: XoUser | undefined, fn: () => void) => Promise<unknown>
|
||||
testRemote(
|
||||
id: XoBackupRepository['id']
|
||||
): Promise<
|
||||
| { success: true; readRate: number; writeRate: number }
|
||||
| { success: false; step: string; file: string; error: unknown }
|
||||
>
|
||||
/** Remove a server from the DB (XCP-ng/XenServer) */
|
||||
unregisterXenServer(id: XoServer['id']): Promise<void>
|
||||
updateUser(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export default {
|
||||
benchmark: true,
|
||||
create: true,
|
||||
read: true,
|
||||
forget: true,
|
||||
|
||||
@@ -20,6 +20,15 @@ export default class Remotes {
|
||||
},
|
||||
],
|
||||
|
||||
ping: [
|
||||
({ remote }) => Disposable.use(this.getHandler(remote), () => ({ success: true })),
|
||||
{
|
||||
params: {
|
||||
remote: { type: 'object' },
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
test: [
|
||||
({ remote }) =>
|
||||
Disposable.use(this.getHandler(remote), handler => handler.test()).catch(error => ({
|
||||
|
||||
@@ -23,11 +23,13 @@ import { forbiddenOperation } from 'xo-common/api-errors.js'
|
||||
|
||||
import { acl, actionsFromBody } from '../middlewares/acl.middleware.mjs'
|
||||
import {
|
||||
asynchronousActionResp,
|
||||
badRequestResp,
|
||||
createdResp,
|
||||
forbiddenOperationResp,
|
||||
invalidParameters,
|
||||
noContentResp,
|
||||
internalServerErrorResp,
|
||||
notFoundResp,
|
||||
unauthorizedResp,
|
||||
type Unbrand,
|
||||
@@ -37,6 +39,8 @@ import {
|
||||
partialBackupRepositories,
|
||||
backupRepository,
|
||||
backupRepositoryId,
|
||||
backupRepositoryBenchmark,
|
||||
backupRepositoryHeath,
|
||||
} from '../open-api/oa-examples/backup-repository.oa-example.mjs'
|
||||
import type { SendObjects } from '../helpers/helper.type.mjs'
|
||||
import { XoController } from '../abstract-classes/xo-controller.mjs'
|
||||
@@ -44,6 +48,9 @@ import { RestApi } from '../rest-api/rest-api.mjs'
|
||||
import { BackupRepositoryService } from './backup-repository.service.mjs'
|
||||
import { CreateActionReturnType } from '../abstract-classes/base-controller.mjs'
|
||||
import { taskLocation } from '../open-api/oa-examples/task.oa-example.mjs'
|
||||
import { ApiError } from '../helpers/error.helper.mjs'
|
||||
|
||||
type BenchmarkRepositoryResult = Awaited<ReturnType<XoApp['testRemote']>>
|
||||
|
||||
@Route('backup-repositories')
|
||||
@Security('*')
|
||||
@@ -224,4 +231,87 @@ export class BackupRepositoryController extends XoController<XoBackupRepository>
|
||||
): Promise<void> {
|
||||
await this.restApi.xoApp.updateRemote(id as XoBackupRepository['id'], body as Parameters<XoApp['updateRemote']>[1])
|
||||
}
|
||||
|
||||
/**
|
||||
* Pings the backup-repository to check its health
|
||||
*
|
||||
* Required privilege:
|
||||
* - resource: backup-repository, action: read
|
||||
*
|
||||
* @example id "c4284e12-37c9-7967-b9e8-83ef229c3e03"
|
||||
*/
|
||||
@Example(backupRepositoryHeath)
|
||||
@Extension('x-mcp-exposure', 'allow')
|
||||
@Get('{id}/health')
|
||||
@Middlewares(
|
||||
acl({
|
||||
resource: 'backup-repository',
|
||||
action: 'read',
|
||||
objectId: 'params.id',
|
||||
getObject: ({ restApi }) => restApi.xoApp.getRemote,
|
||||
})
|
||||
)
|
||||
@SuccessResponse(200, 'OK')
|
||||
@Response(forbiddenOperationResp.status, forbiddenOperationResp.description)
|
||||
@Response(internalServerErrorResp.status, internalServerErrorResp.description)
|
||||
async getBackupRepositoryHealth(@Path() id: string): ReturnType<XoApp['pingRemote']> {
|
||||
return this.restApi.xoApp.pingRemote(id as XoBackupRepository['id'])
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a benchmark for write and read speed on the Backup-repository.
|
||||
* Saves the benchmark result on the BR and returns the results, speeds are in bytes/sec.
|
||||
* 502 if the BR cannot be reached, 400 if there was a problem during the benchmark with the error.
|
||||
*
|
||||
* Required privilege:
|
||||
* - resource: backup-repository, action: benchmark
|
||||
*
|
||||
* @example id "c4284e12-37c9-7967-b9e8-83ef229c3e03"
|
||||
*/
|
||||
@Example(taskLocation)
|
||||
@Example(backupRepositoryBenchmark)
|
||||
@Extension('x-mcp-exposure', 'confirm')
|
||||
@Post('{id}/actions/benchmark')
|
||||
@Middlewares(
|
||||
acl({
|
||||
resource: 'backup-repository',
|
||||
action: 'benchmark',
|
||||
objectId: 'params.id',
|
||||
getObject: ({ restApi }) => restApi.xoApp.getRemote,
|
||||
})
|
||||
)
|
||||
@SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description)
|
||||
@Response(200, 'Ok')
|
||||
@Response(400, 'Benchmark failed')
|
||||
@Response(notFoundResp.status, notFoundResp.description)
|
||||
@Response(forbiddenOperationResp.status, forbiddenOperationResp.description)
|
||||
@Response(internalServerErrorResp.status, internalServerErrorResp.description)
|
||||
@Response(502, 'Backup repository unreachable')
|
||||
benchmarkBackupRepository(
|
||||
@Path() id: string,
|
||||
@Query() sync?: boolean
|
||||
): CreateActionReturnType<BenchmarkRepositoryResult> {
|
||||
const backupRepositoryId = id as XoBackupRepository['id']
|
||||
const action = async () => {
|
||||
let result: BenchmarkRepositoryResult
|
||||
try {
|
||||
result = await this.restApi.xoApp.testRemote(backupRepositoryId)
|
||||
} catch (error) {
|
||||
throw new ApiError('Backup repository unreachable', 502)
|
||||
}
|
||||
if (!result.success) {
|
||||
throw new ApiError('Benchmark failed', 400, { data: result })
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
return this.createAction<BenchmarkRepositoryResult>(action, {
|
||||
sync,
|
||||
statusCode: 200,
|
||||
taskProperties: {
|
||||
name: 'benchmark backup repository',
|
||||
objectId: backupRepositoryId,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,3 +33,11 @@ export const backupRepository = {
|
||||
}
|
||||
|
||||
export const backupRepositoryId = { id: '677e50c5-8d8a-4c89-b1ac-e2f4593d0ebb' }
|
||||
|
||||
export const backupRepositoryHeath = { success: true }
|
||||
|
||||
export const backupRepositoryBenchmark = {
|
||||
success: true,
|
||||
readRate: 7999965,
|
||||
writeRate: 7767798,
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
|
||||
> Users must be able to say: "Nice enhancement, I'm eager to test it"
|
||||
|
||||
- [REST API] Expose `GET /backup-repositories/:id/health` and `POST /backup-repositories/:id/actions/benchmark` routes (PR [#9847](https://github.com/vatesfr/xen-orchestra/pull/9847))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
> Users must be able to say: "I had this issue, happy to know it's fixed"
|
||||
@@ -31,4 +33,10 @@
|
||||
|
||||
<!--packages-start-->
|
||||
|
||||
- @vates/types minor
|
||||
- @xen-orchestra/acl minor
|
||||
- @xen-orchestra/proxy minor
|
||||
- @xen-orchestra/rest-api minor
|
||||
- xo-server minor
|
||||
|
||||
<!--packages-end-->
|
||||
|
||||
@@ -134,19 +134,19 @@ Actions are written using the exact string you pass in a privilege. A parent act
|
||||
|
||||
### XO management resources
|
||||
|
||||
| Resource | Available actions |
|
||||
| ------------------- | ---------------------------------------------------------------------------------------------------------- |
|
||||
| `backup-job` | `read` |
|
||||
| `backup-archive` | `read` |
|
||||
| `backup-log` | `read` |
|
||||
| `backup-repository` | `create`, `read`,`forget`, `update:enabled`, `update:name`, `update:options`, `update:proxy`, `update:url` |
|
||||
| `schedule` | `read`, `run` |
|
||||
| `restore-log` | `read` |
|
||||
| `proxy` | `read` |
|
||||
| `server` | `read`, `create`, `delete`, `connect`, `disconnect` |
|
||||
| `task` | `read`, `abort`, `delete` |
|
||||
| `alarm` | `read` |
|
||||
| `message` | `read` |
|
||||
| Resource | Available actions |
|
||||
| ------------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `backup-job` | `read` |
|
||||
| `backup-archive` | `read` |
|
||||
| `backup-log` | `read` |
|
||||
| `backup-repository` | `benchmark`, `create`, `read`, `forget`, `update:enabled`, `update:name`, `update:options`, `update:proxy`, `update:url` |
|
||||
| `schedule` | `read`, `run` |
|
||||
| `restore-log` | `read` |
|
||||
| `proxy` | `read` |
|
||||
| `server` | `read`, `create`, `delete`, `connect`, `disconnect` |
|
||||
| `task` | `read`, `abort`, `delete` |
|
||||
| `alarm` | `read` |
|
||||
| `message` | `read` |
|
||||
|
||||
### User management resources
|
||||
|
||||
|
||||
@@ -126,21 +126,33 @@ export default class {
|
||||
return handler
|
||||
}
|
||||
|
||||
async pingRemote(remoteId) {
|
||||
const remote = await this.getRemoteWithCredentials(remoteId)
|
||||
|
||||
remote.proxy !== undefined
|
||||
? await this._app.callProxyMethod(remote.proxy, 'remote.ping', {
|
||||
remote,
|
||||
})
|
||||
: await this.getRemoteHandler(remoteId)
|
||||
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
async testRemote(remoteId) {
|
||||
const remote = await this.getRemoteWithCredentials(remoteId)
|
||||
|
||||
const { readRate, writeRate, ...answer } =
|
||||
const result =
|
||||
remote.proxy !== undefined
|
||||
? await this._app.callProxyMethod(remote.proxy, 'remote.test', {
|
||||
remote,
|
||||
})
|
||||
: await this.getRemoteHandler(remoteId).then(handler => handler.test())
|
||||
|
||||
if (answer.success) {
|
||||
if (result.success) {
|
||||
const benchmark = {
|
||||
readRate,
|
||||
readRate: result.readRate,
|
||||
timestamp: Date.now(),
|
||||
writeRate,
|
||||
writeRate: result.writeRate,
|
||||
}
|
||||
await this._updateRemote(remoteId, {
|
||||
error: null,
|
||||
@@ -151,11 +163,11 @@ export default class {
|
||||
})
|
||||
} else {
|
||||
await this._updateRemote(remoteId, {
|
||||
error: answer.error,
|
||||
error: result.error,
|
||||
})
|
||||
}
|
||||
|
||||
return answer
|
||||
return result
|
||||
}
|
||||
|
||||
async getAllRemotesInfo() {
|
||||
|
||||
Reference in New Issue
Block a user