feat(rest): add revert snapshot route (#9788)

This commit is contained in:
spacotte-vates
2026-05-19 10:53:05 +02:00
committed by GitHub
parent 64422066c2
commit 16d1c7ce9d
7 changed files with 73 additions and 3 deletions

View File

@@ -37,6 +37,7 @@ import type {
XoVm,
XoVmTemplate,
XoVif,
XoVmSnapshot,
} from '../xo.mjs'
export type XcpPatches = {
@@ -129,6 +130,7 @@ export interface Xapi {
listMissingPatches(host: XoHost['id']): Promise<XcpPatches[] | XsPatches[]>
pool_emergencyShutdown(): Promise<void>
resumeVm(id: XoVm['id']): Promise<void>
revertVm(snapshotId: XoVmSnapshot['id']): Promise<void>
unpauseVm(id: XoVm['id']): Promise<void>
cloneVm(
vmId: XoVm['id'],

View File

@@ -8,6 +8,7 @@ export default {
hard: true,
},
resume: true,
'revert-snapshot': true,
shutdown: {
clean: true,
hard: true,

View File

@@ -129,7 +129,7 @@ export class GroupController extends XoController<XoGroup> {
json(),
acl({
resource: 'group',
action: actionFromBody('update:name'),
action: actionFromBody<'group'>('update:name'),
objectId: 'params.id',
getObject: ({ restApi }) => restApi.xoApp.getGroup,
}),

View File

@@ -72,7 +72,7 @@ export type AclEntry = {
| {
action:
| SupportedActions<Resource>
| ((opts: { req: AuthenticatedRequest; restApi: RestApi }) => SupportedActions<Resource>)
| ((opts: { req: AuthenticatedRequest; restApi: RestApi }) => SupportedActions<Resource> | undefined)
actions?: never
}
| {

View File

@@ -67,6 +67,7 @@ import { partialVmBackupJobs, vmBackupJobIds } from '../open-api/oa-examples/bac
import { messageIds, partialMessages } from '../open-api/oa-examples/message.oa-example.mjs'
import type { UnbrandedVmDashboard } from './vm.type.mjs'
import type { CreateActionReturnType } from '../abstract-classes/base-controller.mjs'
import { Task } from '@vates/task'
const IGNORED_VDIS_TAG = '[NOSNAP]'
@@ -571,6 +572,71 @@ export class VmController extends XapiXoController<XoVm> {
})
}
/**
* Required privilege:
* - resource: vm, action: revert-snapshot
* - resource: vm, action: snapshot (if `snapshotBefore: true`)
*
* @example id "f07ab729-c0e8-721c-45ec-f11276377030"
* @example body { "snapshotId": "f07ab729-c0e8-721c-45ec-f11276377030", "snapshotBefore": true }
*/
@Example(taskLocation)
@Post('{id}/actions/revert_snapshot')
@Middlewares([
json(),
acl([
{ resource: 'vm', action: 'revert-snapshot', objectId: 'params.id' },
{
resource: 'vm',
action: ({ req }) => (req.body.snapshotBefore ? 'snapshot' : undefined),
objectId: 'params.id',
},
]),
])
@SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description)
@Response(noContentResp.status, noContentResp.description)
@Response(forbiddenOperationResp.status, forbiddenOperationResp.description)
@Response(notFoundResp.status, notFoundResp.description)
@Response(invalidParametersResp.status, invalidParametersResp.description)
@Response(internalServerErrorResp.status, internalServerErrorResp.description)
async revertSnapshotVm(
@Path() id: string,
@Body() body: { snapshotId: string; snapshotBefore?: boolean },
@Query() sync?: boolean
): CreateActionReturnType<void> {
const vmId = id as XoVm['id']
const action = async () => {
const snapshotId = body.snapshotId as XoVmSnapshot['id']
// Ensure the snapshot belongs to this VM
const vm = this.getObject(vmId)
if (!vm.snapshots.includes(snapshotId)) {
throw invalidParameters(`snapshot ${snapshotId} does not belong to VM ${vmId}`)
}
if (body.snapshotBefore) {
await Task.run({ properties: { name: 'snapshot VM', objectId: vmId, objectType: 'VM' } }, () =>
this.getXapiObject(vmId).$snapshot({ ignoredVdisTag: IGNORED_VDIS_TAG })
)
await Task.run({ properties: { name: 'revert snapshot', objectId: vmId, objectType: 'VM' } }, () =>
this.getXapi(vmId).revertVm(snapshotId)
)
} else {
await this.getXapi(vmId).revertVm(snapshotId)
}
}
return this.createAction<void>(action, {
sync,
statusCode: noContentResp.status,
taskProperties: {
name: 'revert VM snapshot',
objectId: vmId,
},
})
}
/**
* Required privilege:
* - resource: vm, action: snapshot

View File

@@ -18,6 +18,7 @@
- [xo-server] support qcow2 format in `disk.importContent` and `disk.import` jsonRPC api (PR [#9817](https://github.com/vatesfr/xen-orchestra/pull/9817))
- [web-core] Update `UiTag` and parse tag for detecting tags with `=` (PR [#9811](https://github.com/vatesfr/xen-orchestra/pull/9811))
- [Encryption] Implement encryption and decryption feature for redis (PR [#9735](https://github.com/vatesfr/xen-orchestra/pull/9735))
- [REST API] Add `vms/:id/actions/revert_snapshot` REST route (PR [#9788](https://github.com/vatesfr/xen-orchestra/pull/9788))
- **XO 5**:
- [Export config] Hide passphrase by default (PR [#9824](https://github.com/vatesfr/xen-orchestra/pull/9824))
- [Backups] Refactor clean phase for incremental and full backups ([#9765](https://github.com/vatesfr/xen-orchestra/pull/9765))

View File

@@ -105,7 +105,7 @@ Actions are written using the exact string you pass in a privilege. A parent act
| Resource | Available actions |
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `vm` | `read`, `delete`, `export`, `pause`, `start`, `resume`, `snapshot`, `suspend`, `unpause`, `reboot:clean`, `reboot:hard`, `shutdown:clean`, `shutdown:hard`, `update:datasources`, `update:tags` |
| `vm` | `read`, `delete`, `export`, `pause`, `start`, `resume`, `revert-snapshot`, `snapshot`, `suspend`, `unpause`, `reboot:clean`, `reboot:hard`, `shutdown:clean`, `shutdown:hard`, `update:datasources`, `update:tags` |
| `vm-snapshot` | `read`, `delete`, `export`, `update:tags` |
| `vm-template` | `read`, `delete`, `export`, `instantiate`, `update:tags` |
| `vm-controller` | `read`, `update:tags` |