@xen-orchestra/rest-api 0.32.0 → 0.33.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.
@@ -7,13 +7,14 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  var __param = (this && this.__param) || function (paramIndex, decorator) {
8
8
  return function (target, key) { decorator(target, key, paramIndex); }
9
9
  };
10
- import { Body, Delete, Example, Extension, Get, Middlewares, Path, Post, Put, Query, Request, Response, Route, Security, SuccessResponse, Tags, } from 'tsoa';
10
+ import { Body, Delete, Example, Extension, Get, Middlewares, Patch, Path, Post, Put, Query, Request, Response, Route, Security, SuccessResponse, Tags, } from 'tsoa';
11
11
  import { json } from 'express';
12
12
  import { inject } from 'inversify';
13
13
  import { incorrectState, invalidParameters } from 'xo-common/api-errors.js';
14
14
  import { provide } from 'inversify-binding-decorators';
15
15
  import { PassThrough } from 'node:stream';
16
- import { acl } from '../middlewares/acl.middleware.mjs';
16
+ import { SUPPORTED_ACTIONS_BY_RESOURCE } from '@xen-orchestra/acl';
17
+ import { acl, actionsFromBody } from '../middlewares/acl.middleware.mjs';
17
18
  import { asynchronousActionResp, badRequestResp, createdResp, forbiddenOperationResp, incorrectStateResp, internalServerErrorResp, invalidParameters as invalidParametersResp, noContentResp, notFoundResp, unauthorizedResp, } from '../open-api/common/response.common.mjs';
18
19
  import { BASE_URL } from '../index.mjs';
19
20
  import { limitAndFilterArray, NDJSON_CONTENT_TYPE } from '../helpers/utils.helper.mjs';
@@ -28,6 +29,11 @@ import { partialVmBackupJobs, vmBackupJobIds } from '../open-api/oa-examples/bac
28
29
  import { messageIds, partialMessages } from '../open-api/oa-examples/message.oa-example.mjs';
29
30
  import { Task } from '@vates/task';
30
31
  const IGNORED_VDIS_TAG = '[NOSNAP]';
32
+ // `datasources` is managed through the dedicated `/vms/{id}/stats/data_source`
33
+ // endpoints, not as a direct VM property, so it cannot be updated via PATCH /vms.
34
+ const UPDATE_VM_ACTIONS = Object.keys(SUPPORTED_ACTIONS_BY_RESOURCE.vm.update)
35
+ .filter(action => action !== 'datasources')
36
+ .map(k => `update:${k}`);
31
37
  let VmController = class VmController extends XapiXoController {
32
38
  #vmService;
33
39
  #backupJobService;
@@ -74,6 +80,29 @@ let VmController = class VmController extends XapiXoController {
74
80
  getVm(id) {
75
81
  return this.getObject(id);
76
82
  }
83
+ /**
84
+ * Partial update of a VM. Only the fields present in the body are modified;
85
+ * everything else is left untouched.
86
+ *
87
+ * Operations are applied sequentially: if one fails, previously applied
88
+ * changes are not rolled back.
89
+ *
90
+ * Required privilege per field provided in the body:
91
+ * - resource: vm, action: update:<field> (e.g. update:nameLabel, update:cpus, ...)
92
+ *
93
+ * Special fields:
94
+ * - `xenStoreData` keys are automatically prefixed with `vm-data/` when missing
95
+ *
96
+ * @example id "f07ab729-c0e8-721c-45ec-f11276377030"
97
+ * @example body {
98
+ * "nameLabel": "web-prod-01",
99
+ * "nameDescription": "Production web frontend — managed by n8n",
100
+ * "notes": "Docker containers: nginx, app-1, app-2"
101
+ * }
102
+ */
103
+ async updateVm(id, body) {
104
+ await this.#vmService.updateVm(id, body);
105
+ }
77
106
  /**
78
107
  * Required privilege:
79
108
  * - resource: vm, action: delete
@@ -663,6 +692,24 @@ __decorate([
663
692
  Response(notFoundResp.status, notFoundResp.description),
664
693
  __param(0, Path())
665
694
  ], VmController.prototype, "getVm", null);
695
+ __decorate([
696
+ Patch('{id}'),
697
+ Middlewares([
698
+ json(),
699
+ acl({
700
+ resource: 'vm',
701
+ actions: actionsFromBody(UPDATE_VM_ACTIONS),
702
+ objectId: 'params.id',
703
+ }),
704
+ ]),
705
+ SuccessResponse(noContentResp.status, noContentResp.description),
706
+ Response(forbiddenOperationResp.status, forbiddenOperationResp.description),
707
+ Response(notFoundResp.status, notFoundResp.description),
708
+ Response(invalidParametersResp.status, invalidParametersResp.description),
709
+ Response(internalServerErrorResp.status, internalServerErrorResp.description),
710
+ __param(0, Path()),
711
+ __param(1, Body())
712
+ ], VmController.prototype, "updateVm", null);
666
713
  __decorate([
667
714
  Extension('x-mcp-exposure', 'confirm'),
668
715
  Delete('{id}'),
@@ -146,6 +146,23 @@ export class VmService {
146
146
  });
147
147
  return alarms;
148
148
  }
149
+ async updateVm(id, body) {
150
+ const { resourceSet, share, ...editProps } = body;
151
+ // Touch the object so 404 is raised before any side effect.
152
+ void this.#restApi.getObject(id, 'VM');
153
+ const xoApp = this.#restApi.xoApp;
154
+ if (resourceSet !== undefined) {
155
+ await xoApp.setVmResourceSet(id, resourceSet, true);
156
+ }
157
+ else if (share) {
158
+ // `share: false` is a no-op.
159
+ await xoApp.shareVmResourceSet(id);
160
+ }
161
+ if (Object.keys(editProps).length === 0) {
162
+ return;
163
+ }
164
+ await xoApp.getXapi(id).editVm(id, editProps);
165
+ }
149
166
  #getDashboardQuickInfo(id) {
150
167
  const { power_state, uuid, name_description, CPUs, mainIpAddress, os_version, memory, creation, $pool, virtualizationMode, tags, $container, startTime, pvDriversDetected, pvDriversVersion, pvDriversUpToDate, } = this.#restApi.getObject(id, 'VM');
151
168
  return {