Compare commits

...

8 Commits

Author SHA1 Message Date
Pierre Brunet
ab8b37587c feat(restapi): remove jobs legacy code 2025-09-10 10:54:31 +02:00
Pierre Brunet
ab194deedf feat(restapi/jobs): add redirection 2025-09-10 10:52:24 +02:00
Pierre Brunet
89c42a5f96 fix(rest-api): small updates 2025-09-10 10:36:04 +02:00
Pierre Brunet
81ecec19c8 fix(@xen-orchestra/restapi): imports 2025-09-10 10:33:48 +02:00
Pierre Brunet
e180bdc3cf fix(@xen-orchestra/restapi): updates from reviews 2025-09-10 10:11:19 +02:00
Pierre Brunet
267f0f0306 fix(restapi): remove useless code 2025-09-10 10:09:17 +02:00
Pierre Brunet
d6e6babb26 fix(restapi): update unbranded jobs and changelog 2025-09-10 10:09:17 +02:00
Pierre Brunet
821dfdb710 feat(@xen-orchestra/restapi): add jobs to api 2025-09-10 10:07:33 +02:00
7 changed files with 276 additions and 46 deletions

View File

@@ -0,0 +1,22 @@
import { BACKUP_TYPE, XoBackupJob, XoMetadataBackupJob, XoMirrorBackupJob } from '@vates/types'
import { RestApi } from '../rest-api/rest-api.mjs'
export class JobService {
#restApi: RestApi
constructor(restApi: RestApi) {
this.#restApi = restApi
}
async getVmJobs(): Promise<XoBackupJob[]> {
return this.#restApi.xoApp.getAllJobs(BACKUP_TYPE.backup)
}
async getMetadataJobs(): Promise<XoMetadataBackupJob[]> {
return this.#restApi.xoApp.getAllJobs(BACKUP_TYPE.metadata)
}
async getMirrorJobs(): Promise<XoMirrorBackupJob[]> {
return this.#restApi.xoApp.getAllJobs(BACKUP_TYPE.mirror)
}
}

View File

@@ -0,0 +1,61 @@
import { Example, Get, Path, Query, Request, Response, Route, Security, Tags } from 'tsoa'
import { provide } from 'inversify-binding-decorators'
import { Request as ExRequest } from 'express'
import type { Branded, XoJob, XoMetadataBackupJob } from '@vates/types'
import { notFoundResp, type Unbrand } from '../../open-api/common/response.common.mjs'
import type { SendObjects } from '../../helpers/helper.type.mjs'
import { XoController } from '../../abstract-classes/xo-controller.mjs'
import { RestApi } from '../../rest-api/rest-api.mjs'
import { inject } from 'inversify'
import { JobService } from '../jobs.service.mjs'
import { job, jobIds, partialJobs } from '../../open-api/oa-examples/jobs.oa-example.mjs'
type UnbrandedXoMetadataJob = Unbrand<Omit<XoMetadataBackupJob, 'settings'>>
@Security('*')
@Route('backup/jobs')
@Response(notFoundResp.status, notFoundResp.description)
@Tags('backup')
@provide(MetadataJobController)
export class MetadataJobController extends XoController<XoMetadataBackupJob> {
#jobService: JobService
constructor(@inject(RestApi) restApi: RestApi) {
super(restApi)
this.#jobService = new JobService(this.restApi)
}
async getAllCollectionObjects(): Promise<XoMetadataBackupJob[]> {
return this.#jobService.getMetadataJobs()
}
async getCollectionObject(id: Branded<'job'>): Promise<XoMetadataBackupJob> {
return this.restApi.xoApp.getJob(id) as Promise<XoMetadataBackupJob>
}
/**
* @example fields "name,type,remotes"
* @example filter "name:test"
* @example limit 42
*/
@Example(jobIds)
@Example(partialJobs)
@Get('metadata')
async getMetadataJobs(
@Request() req: ExRequest,
@Query() fields?: string,
@Query() ndjson?: boolean,
@Query() filter?: string,
@Query() limit?: number
): Promise<SendObjects<UnbrandedXoMetadataJob>> {
return this.sendObjects(Object.values(await this.getObjects({ filter, limit })), req)
}
/**
* @example id "7d98fee4-3357-41a7-ac3f-9124212badb7"
*/
@Get('metadata/{id}')
@Response(notFoundResp.status, notFoundResp.description)
async getMetadataJob(@Path() id: string): Promise<UnbrandedXoMetadataJob> {
return this.getObject(id as XoJob['id'])
}
}

View File

@@ -0,0 +1,62 @@
import { Example, Get, Path, Query, Request, Response, Route, Security, Tags } from 'tsoa'
import { provide } from 'inversify-binding-decorators'
import { Request as ExRequest } from 'express'
import type { Branded, XoJob, XoMirrorBackupJob } from '@vates/types'
import { notFoundResp, type Unbrand } from '../../open-api/common/response.common.mjs'
import type { SendObjects } from '../../helpers/helper.type.mjs'
import { XoController } from '../../abstract-classes/xo-controller.mjs'
import { RestApi } from '../../rest-api/rest-api.mjs'
import { inject } from 'inversify'
import { JobService } from '../jobs.service.mjs'
import { job, jobIds, partialJobs } from '../../open-api/oa-examples/jobs.oa-example.mjs'
type UnbrandedXoMirrorJob = Unbrand<Omit<XoMirrorBackupJob, 'settings'>>
@Security('*')
@Route('backup/jobs')
@Response(notFoundResp.status, notFoundResp.description)
@Tags('backup')
@provide(MirrorJobController)
export class MirrorJobController extends XoController<XoMirrorBackupJob> {
#jobService: JobService
constructor(@inject(RestApi) restApi: RestApi) {
super(restApi)
this.#jobService = new JobService(this.restApi)
}
async getAllCollectionObjects(): Promise<XoMirrorBackupJob[]> {
return this.#jobService.getMirrorJobs()
}
async getCollectionObject(id: Branded<'job'>): Promise<XoMirrorBackupJob> {
return this.restApi.xoApp.getJob(id) as Promise<XoMirrorBackupJob>
}
/**
* @example fields "name,type,remotes"
* @example filter "name:test"
* @example limit 42
*/
@Example(jobIds)
@Example(partialJobs)
@Get('mirror')
async getMirrorJobs(
@Request() req: ExRequest,
@Query() fields?: string,
@Query() ndjson?: boolean,
@Query() filter?: string,
@Query() limit?: number
): Promise<SendObjects<UnbrandedXoMirrorJob>> {
return this.sendObjects(Object.values(await this.getObjects({ filter, limit })), req)
}
/**
* @example id "7d98fee4-3357-41a7-ac3f-9124212badb7"
*/
@Example(job)
@Get('mirror/{id}')
@Response(notFoundResp.status, notFoundResp.description)
async getMirrorJob(@Path() id: string): Promise<UnbrandedXoMirrorJob> {
return this.getObject(id as XoJob['id'])
}
}

View File

@@ -0,0 +1,73 @@
import { Example, Get, Path, Query, Request, Response, Route, Security, SuccessResponse, Tags } from 'tsoa'
import { provide } from 'inversify-binding-decorators'
import { Request as ExRequest, Response as ExResponse } from 'express'
import type { Branded, XoBackupJob, XoJob } from '@vates/types'
import { notFoundResp, type Unbrand } from '../../open-api/common/response.common.mjs'
import type { SendObjects } from '../../helpers/helper.type.mjs'
import { XoController } from '../../abstract-classes/xo-controller.mjs'
import { RestApi } from '../../rest-api/rest-api.mjs'
import { inject } from 'inversify'
import { JobService } from '../jobs.service.mjs'
import { job, jobIds, partialJobs } from '../../open-api/oa-examples/jobs.oa-example.mjs'
type UnbrandedXoBackupJob = Unbrand<Omit<XoBackupJob, 'settings'>>
@Security('*')
@Route('backup/jobs')
@Response(notFoundResp.status, notFoundResp.description)
@Tags('backup')
@provide(VmJobController)
export class VmJobController extends XoController<XoBackupJob> {
#jobService: JobService
constructor(@inject(RestApi) restApi: RestApi) {
super(restApi)
this.#jobService = new JobService(this.restApi)
}
async getAllCollectionObjects(): Promise<XoBackupJob[]> {
return this.#jobService.getVmJobs()
}
async getCollectionObject(id: Branded<'job'>): Promise<XoBackupJob> {
return this.restApi.xoApp.getJob(id) as Promise<XoBackupJob>
}
/**
* @example fields "name,type,remotes"
* @example filter "name:test"
* @example limit 42
*/
@Example(jobIds)
@Example(partialJobs)
@Get('vm')
async getVmJobs(
@Request() req: ExRequest,
@Query() fields?: string,
@Query() ndjson?: boolean,
@Query() filter?: string,
@Query() limit?: number
): Promise<SendObjects<UnbrandedXoBackupJob>> {
return this.sendObjects(Object.values(await this.getObjects({ filter, limit })), req)
}
/**
* @example id "7d98fee4-3357-41a7-ac3f-9124212badb7"
*/
@Example(job)
@Get('vm/{id}')
@Response(notFoundResp.status, notFoundResp.description)
async getVmJob(@Path() id: string): Promise<UnbrandedXoBackupJob> {
return this.getObject(id as XoJob['id'])
}
/**
* Redirects from /backup/jobs/{id} to /backup/jobs/vm/{id}
* @example id "7d98fee4-3357-41a7-ac3f-9124212badb7"
*/
@Get('{id}')
@Response(308, 'Permanent Redirect')
async redirectToVmJob(@Path() id: string, @Request() req: ExRequest): Promise<void> {
const res = req.res as ExResponse
res.redirect(308, `/rest/v0/backup/jobs/vm/${id}`)
}
}

View File

@@ -0,0 +1,50 @@
export const jobIds = [
'/rest/v0/backup/jobs/vm/d2511443-5fef-4750-b59e-7eca01d1eaab',
'/rest/v0/backup/jobs/vm/73a76f43-c5cf-4644-878d-1f6c0e41c8b8',
]
export const partialJobs = [
{
name: 'Azurite delta',
type: 'backup',
remotes: {
id: 'a72c53c1-eae1-43f6-be6d-7b0342a1f218',
},
href: '/rest/v0/backup/jobs/vm/d2511443-5fef-4750-b59e-7eca01d1eaab',
},
{
name: 'Backup QA Auto',
type: 'backup',
remotes: {
id: 'a72c53c1-eae1-43f6-be6d-7b0342a1f218',
},
href: '/rest/v0/backup/jobs/vm/73a76f43-c5cf-4644-878d-1f6c0e41c8b8',
},
]
export const job = {
name: 'Azurite delta',
mode: 'delta',
settings: {
'': {
timezone: 'Europe/Paris',
preferNbd: true,
},
'ee3d0a7c-6f4c-4664-822cdc4953f2a571': {
exportRetention: 1,
},
},
remotes: {
id: 'a72c53c1-eae1-43f6-be6d-7b0342a1f218',
},
vms: {
id: '7b894929-273e-15f4-b7df-88a497d90c14',
},
type: 'backup',
srs: {
id: {
__or: [],
},
},
id: 'd2511443-5fef-4750-b59e-7eca01d1eaab',
}

View File

@@ -22,7 +22,6 @@ import type {
XenApiVtpmWrapped,
} from '@vates/types/xen-api'
import type {
AnyXoJob,
XoBackupRepository,
XoHost,
XoServer,
@@ -36,6 +35,7 @@ import type {
XoTask,
XoProxy,
AnyXoBackupJob,
AnyXoJob,
} from '@vates/types/xo'
import type { InsertableXoServer } from '../servers/server.type.mjs'

View File

@@ -784,14 +784,6 @@ export default class RestApi {
res.redirect(307, req.baseUrl + '/users/' + user.id + (req.params[0] ?? ''))
})
const backupTypes = {
__proto__: null,
metadata: 'metadataBackup',
mirror: 'mirrorBackup',
vm: 'backup',
}
api
.get(
'/backup',
@@ -808,35 +800,6 @@ export default class RestApi {
await sendObjects(logs, req, res)
})
)
.get(
'/backup/jobs',
wrap((req, res) =>
sendObjects(
Object.keys(backupTypes).map(id => ({ id })),
req,
res
)
)
)
for (const [collection, type] of Object.entries(backupTypes)) {
api
.get(
'/backup/jobs/' + collection,
wrap(async (req, res) => sendObjects(await app.getAllJobs(type), req, res))
)
.get(
`/backup/jobs/${collection}/:id`,
wrap(async (req, res) => {
res.json(await app.getJob(req.params.id, type))
}, true)
)
}
// For compatibility, redirect /backup/jobs/:id to /backup/jobs/vm/:id
api.get('/backup/jobs/:id', (req, res) => {
res.redirect(308, req.baseUrl + '/backup/jobs/vm/' + req.params.id)
})
api
.get(
@@ -861,15 +824,14 @@ export default class RestApi {
}, true)
)
api
.get(
'/tasks/:id/actions',
wrap(async (req, res) => {
const task = await app.tasks.get(req.params.id)
api.get(
'/tasks/:id/actions',
wrap(async (req, res) => {
const task = await app.tasks.get(req.params.id)
await sendObjects(task.status === 'pending' ? [{ id: 'abort' }] : [], req, res)
})
)
await sendObjects(task.status === 'pending' ? [{ id: 'abort' }] : [], req, res)
})
)
api.get(
'/:collection',