You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cloudstack.apache.org by da...@apache.org on 2026/04/13 08:29:12 UTC
(cloudstack) 10/25: extensions: allow reserved resource details
This is an automated email from the ASF dual-hosted git repository.
dahn pushed a commit to branch 22.0.1-fixes
in repository https://gitbox.apache.org/repos/asf/cloudstack.git
commit 95816b44e93341e63688c66da7d0673acb59c953
Author: Abhishek Kumar <ab...@gmail.com>
AuthorDate: Thu Jan 15 10:22:28 2026 +0530
extensions: allow reserved resource details
Adds a new request parameter for create/updateExtension API to allow
operator to provide detail names for the extension resources which will be reserved to be used by the extension. The end user won't be able to view or add details with these details names for the resource.
Signed-off-by: Abhishek Kumar <ab...@gmail.com>
---
.../org/apache/cloudstack/api/ApiConstants.java | 1 +
.../cloudstack/api/response/ExtensionResponse.java | 10 ++
.../cloudstack/extension/ExtensionHelper.java | 3 +
.../META-INF/db/views/cloud.user_vm_view.sql | 1 +
.../extensions/api/CreateExtensionCmd.java | 10 ++
.../extensions/api/UpdateExtensionCmd.java | 10 ++
.../extensions/manager/ExtensionsManagerImpl.java | 98 ++++++++++--
.../extensions/api/CreateExtensionCmdTest.java | 14 ++
.../extensions/api/UpdateExtensionCmdTest.java | 15 ++
.../manager/ExtensionsManagerImplTest.java | 167 +++++++++++++++++++--
.../com/cloud/api/query/dao/UserVmJoinDaoImpl.java | 25 ++-
.../java/com/cloud/api/query/vo/UserVmJoinVO.java | 7 +
.../main/java/com/cloud/vm/UserVmManagerImpl.java | 9 ++
.../cloud/api/query/dao/UserVmJoinDaoImplTest.java | 4 +
ui/public/locales/en.json | 1 +
ui/src/config/section/extension.js | 2 +-
ui/src/views/extension/CreateExtension.vue | 11 ++
ui/src/views/extension/UpdateExtension.vue | 17 ++-
18 files changed, 371 insertions(+), 34 deletions(-)
diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
index 8fca652518f..4fa5f595308 100644
--- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
+++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
@@ -502,6 +502,7 @@ public class ApiConstants {
public static final String RECOVER = "recover";
public static final String REPAIR = "repair";
public static final String REQUIRES_HVM = "requireshvm";
+ public static final String RESERVED_RESOURCE_DETAILS = "reservedresourcedetails";
public static final String RESOURCES = "resources";
public static final String RESOURCE_COUNT = "resourcecount";
public static final String RESOURCE_NAME = "resourcename";
diff --git a/api/src/main/java/org/apache/cloudstack/api/response/ExtensionResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/ExtensionResponse.java
index fdf1e87df50..911839da405 100644
--- a/api/src/main/java/org/apache/cloudstack/api/response/ExtensionResponse.java
+++ b/api/src/main/java/org/apache/cloudstack/api/response/ExtensionResponse.java
@@ -85,6 +85,12 @@ public class ExtensionResponse extends BaseResponse {
@Param(description = "Removal timestamp of the extension, if applicable")
private Date removed;
+ @SerializedName(ApiConstants.RESERVED_RESOURCE_DETAILS)
+ @Param(description = "Resource detail names as comma separated string that should be reserved and not visible " +
+ "to end users",
+ since = "4.22.1")
+ protected String reservedResourceDetails;
+
public ExtensionResponse(String id, String name, String description, String type) {
this.id = id;
this.name = name;
@@ -179,4 +185,8 @@ public class ExtensionResponse extends BaseResponse {
public void setRemoved(Date removed) {
this.removed = removed;
}
+
+ public void setReservedResourceDetails(String reservedResourceDetails) {
+ this.reservedResourceDetails = reservedResourceDetails;
+ }
}
diff --git a/api/src/main/java/org/apache/cloudstack/extension/ExtensionHelper.java b/api/src/main/java/org/apache/cloudstack/extension/ExtensionHelper.java
index f50f841ed74..a01131278a7 100644
--- a/api/src/main/java/org/apache/cloudstack/extension/ExtensionHelper.java
+++ b/api/src/main/java/org/apache/cloudstack/extension/ExtensionHelper.java
@@ -17,8 +17,11 @@
package org.apache.cloudstack.extension;
+import java.util.List;
+
public interface ExtensionHelper {
Long getExtensionIdForCluster(long clusterId);
Extension getExtension(long id);
Extension getExtensionForCluster(long clusterId);
+ List<String> getExtensionReservedResourceDetails(long extensionId);
}
diff --git a/engine/schema/src/main/resources/META-INF/db/views/cloud.user_vm_view.sql b/engine/schema/src/main/resources/META-INF/db/views/cloud.user_vm_view.sql
index 94bc8640fd5..34aa59075ac 100644
--- a/engine/schema/src/main/resources/META-INF/db/views/cloud.user_vm_view.sql
+++ b/engine/schema/src/main/resources/META-INF/db/views/cloud.user_vm_view.sql
@@ -79,6 +79,7 @@ SELECT
`vm_template`.`format` AS `template_format`,
`vm_template`.`display_text` AS `template_display_text`,
`vm_template`.`enable_password` AS `password_enabled`,
+ `vm_template`.`extension_id` AS `template_extension_id`,
`iso`.`id` AS `iso_id`,
`iso`.`uuid` AS `iso_uuid`,
`iso`.`name` AS `iso_name`,
diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/CreateExtensionCmd.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/CreateExtensionCmd.java
index 5ab54149645..9d76a7e6ec2 100644
--- a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/CreateExtensionCmd.java
+++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/CreateExtensionCmd.java
@@ -83,6 +83,12 @@ public class CreateExtensionCmd extends BaseCmd {
description = "Details in key/value pairs using format details[i].keyname=keyvalue. Example: details[0].endpoint.url=urlvalue")
protected Map details;
+ @Parameter(name = ApiConstants.RESERVED_RESOURCE_DETAILS, type = CommandType.STRING,
+ description = "Resource detail names as comma separated string that should be reserved and not visible " +
+ "to end users",
+ since = "4.22.1")
+ protected String reservedResourceDetails;
+
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
@@ -115,6 +121,10 @@ public class CreateExtensionCmd extends BaseCmd {
return convertDetailsToMap(details);
}
+ public String getReservedResourceDetails() {
+ return reservedResourceDetails;
+ }
+
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/UpdateExtensionCmd.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/UpdateExtensionCmd.java
index ded07d2dd32..5baaea1709d 100644
--- a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/UpdateExtensionCmd.java
+++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/UpdateExtensionCmd.java
@@ -78,6 +78,12 @@ public class UpdateExtensionCmd extends BaseCmd {
"if false or not set, no action)")
private Boolean cleanupDetails;
+ @Parameter(name = ApiConstants.RESERVED_RESOURCE_DETAILS, type = CommandType.STRING,
+ description = "Resource detail names as comma separated string that should be reserved and not visible " +
+ "to end users",
+ since = "4.22.1")
+ protected String reservedResourceDetails;
+
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
@@ -106,6 +112,10 @@ public class UpdateExtensionCmd extends BaseCmd {
return cleanupDetails;
}
+ public String getReservedResourceDetails() {
+ return reservedResourceDetails;
+ }
+
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManagerImpl.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManagerImpl.java
index 4171b9615fe..1422338ddc9 100644
--- a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManagerImpl.java
+++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManagerImpl.java
@@ -216,6 +216,11 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
@Inject
AccountService accountService;
+ // Map of in-built extension names and their reserved resource details that shouldn't be accessible to end-users
+ protected static final Map<String, List<String>> INBUILT_RESERVED_RESOURCE_DETAILS = Map.of(
+ "proxmox", List.of("proxmox_vmid")
+ );
+
private ScheduledExecutorService extensionPathStateCheckExecutor;
protected String getDefaultExtensionRelativePath(String name) {
@@ -563,6 +568,25 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
updateExtensionPathReady(extension, true);
}
+ protected void addInbuiltExtensionReservedResourceDetails(long extensionId, List<String> reservedResourceDetails) {
+ ExtensionVO vo = extensionDao.findById(extensionId);
+ if (vo == null || vo.isUserDefined()) {
+ return;
+ }
+ String lowerName = StringUtils.defaultString(vo.getName()).toLowerCase();
+ Optional<Map.Entry<String, List<String>>> match = INBUILT_RESERVED_RESOURCE_DETAILS.entrySet().stream()
+ .filter(e -> lowerName.contains(e.getKey().toLowerCase()))
+ .findFirst();
+ if (match.isPresent()) {
+ Set<String> existing = new HashSet<>(reservedResourceDetails);
+ for (String detailKey : match.get().getValue()) {
+ if (existing.add(detailKey)) {
+ reservedResourceDetails.add(detailKey);
+ }
+ }
+ }
+ }
+
@Override
public String getExtensionsPath() {
return externalProvisioner.getExtensionsPath();
@@ -577,6 +601,7 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
String relativePath = cmd.getPath();
final Boolean orchestratorRequiresPrepareVm = cmd.isOrchestratorRequiresPrepareVm();
final String stateStr = cmd.getState();
+ final String reservedResourceDetails = cmd.getReservedResourceDetails();
ExtensionVO extensionByName = extensionDao.findByName(name);
if (extensionByName != null) {
throw new CloudRuntimeException("Extension by name already exists");
@@ -624,6 +649,10 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM, String.valueOf(orchestratorRequiresPrepareVm),
false));
}
+ if (StringUtils.isNotBlank(reservedResourceDetails)) {
+ detailsVOList.add(new ExtensionDetailsVO(extension.getId(),
+ ApiConstants.RESERVED_RESOURCE_DETAILS, reservedResourceDetails, false));
+ }
if (CollectionUtils.isNotEmpty(detailsVOList)) {
extensionDetailsDao.saveDetails(detailsVOList);
}
@@ -704,6 +733,7 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
final String stateStr = cmd.getState();
final Map<String, String> details = cmd.getDetails();
final Boolean cleanupDetails = cmd.isCleanupDetails();
+ final String reservedResourceDetails = cmd.getReservedResourceDetails();
final ExtensionVO extensionVO = extensionDao.findById(id);
if (extensionVO == null) {
throw new InvalidParameterValueException("Failed to find the extension");
@@ -732,7 +762,8 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
throw new CloudRuntimeException(String.format("Failed to updated the extension: %s",
extensionVO.getName()));
}
- updateExtensionsDetails(cleanupDetails, details, orchestratorRequiresPrepareVm, id);
+ updateExtensionsDetails(cleanupDetails, details, orchestratorRequiresPrepareVm, reservedResourceDetails,
+ id);
return extensionVO;
});
if (StringUtils.isNotBlank(stateStr)) {
@@ -748,9 +779,11 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
return result;
}
- protected void updateExtensionsDetails(Boolean cleanupDetails, Map<String, String> details, Boolean orchestratorRequiresPrepareVm, long id) {
+ protected void updateExtensionsDetails(Boolean cleanupDetails, Map<String, String> details,
+ Boolean orchestratorRequiresPrepareVm, String reservedResourceDetails, long id) {
final boolean needToUpdateAllDetails = Boolean.TRUE.equals(cleanupDetails) || MapUtils.isNotEmpty(details);
- if (!needToUpdateAllDetails && orchestratorRequiresPrepareVm == null) {
+ if (!needToUpdateAllDetails && orchestratorRequiresPrepareVm == null &&
+ StringUtils.isBlank(reservedResourceDetails)) {
return;
}
if (needToUpdateAllDetails) {
@@ -761,6 +794,9 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
hiddenDetails.put(ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM,
String.valueOf(orchestratorRequiresPrepareVm));
}
+ if (StringUtils.isNotBlank(reservedResourceDetails)) {
+ hiddenDetails.put(ApiConstants.RESERVED_RESOURCE_DETAILS, reservedResourceDetails);
+ }
if (MapUtils.isNotEmpty(hiddenDetails)) {
hiddenDetails.forEach((key, value) -> detailsVOList.add(
new ExtensionDetailsVO(id, key, value, false)));
@@ -775,15 +811,29 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
extensionDetailsDao.removeDetails(id);
}
} else {
- ExtensionDetailsVO detailsVO = extensionDetailsDao.findDetail(id,
- ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM);
- if (detailsVO == null) {
- extensionDetailsDao.persist(new ExtensionDetailsVO(id,
- ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM,
- String.valueOf(orchestratorRequiresPrepareVm), false));
- } else if (Boolean.parseBoolean(detailsVO.getValue()) != orchestratorRequiresPrepareVm) {
- detailsVO.setValue(String.valueOf(orchestratorRequiresPrepareVm));
- extensionDetailsDao.update(detailsVO.getId(), detailsVO);
+ if (orchestratorRequiresPrepareVm != null) {
+ ExtensionDetailsVO detailsVO = extensionDetailsDao.findDetail(id,
+ ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM);
+ if (detailsVO == null) {
+ extensionDetailsDao.persist(new ExtensionDetailsVO(id,
+ ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM,
+ String.valueOf(orchestratorRequiresPrepareVm), false));
+ } else if (Boolean.parseBoolean(detailsVO.getValue()) != orchestratorRequiresPrepareVm) {
+ detailsVO.setValue(String.valueOf(orchestratorRequiresPrepareVm));
+ extensionDetailsDao.update(detailsVO.getId(), detailsVO);
+ }
+ }
+ if (StringUtils.isNotBlank(reservedResourceDetails)) {
+ ExtensionDetailsVO detailsVO = extensionDetailsDao.findDetail(id,
+ ApiConstants.RESERVED_RESOURCE_DETAILS);
+ if (detailsVO == null) {
+ extensionDetailsDao.persist(new ExtensionDetailsVO(id,
+ ApiConstants.RESERVED_RESOURCE_DETAILS,
+ reservedResourceDetails, false));
+ } else if (!reservedResourceDetails.equals(detailsVO.getValue())) {
+ detailsVO.setValue(reservedResourceDetails);
+ extensionDetailsDao.update(detailsVO.getId(), detailsVO);
+ }
}
}
}
@@ -961,12 +1011,16 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
hiddenDetails = extensionDetails.second();
} else {
hiddenDetails = extensionDetailsDao.listDetailsKeyPairs(extension.getId(),
- List.of(ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM));
+ List.of(ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM,
+ ApiConstants.RESERVED_RESOURCE_DETAILS));
}
if (hiddenDetails.containsKey(ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM)) {
response.setOrchestratorRequiresPrepareVm(Boolean.parseBoolean(
hiddenDetails.get(ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM)));
}
+ if (hiddenDetails.containsKey(ApiConstants.RESERVED_RESOURCE_DETAILS)) {
+ response.setReservedResourceDetails(hiddenDetails.get(ApiConstants.RESERVED_RESOURCE_DETAILS));
+ }
response.setObjectName(Extension.class.getSimpleName().toLowerCase());
return response;
}
@@ -1605,6 +1659,24 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
return extensionDao.findById(extensionId);
}
+ @Override
+ public List<String> getExtensionReservedResourceDetails(long extensionId) {
+ ExtensionDetailsVO detailsVO = extensionDetailsDao.findDetail(extensionId,
+ ApiConstants.RESERVED_RESOURCE_DETAILS);
+ if (detailsVO == null || !StringUtils.isNotBlank(detailsVO.getValue())) {
+ return Collections.emptyList();
+ }
+ List<String> reservedDetails = new ArrayList<>();
+ String[] parts = detailsVO.getValue().split(",");
+ for (String part : parts) {
+ if (StringUtils.isNotBlank(part)) {
+ reservedDetails.add(part.trim());
+ }
+ }
+ addInbuiltExtensionReservedResourceDetails(extensionId, reservedDetails);
+ return reservedDetails;
+ }
+
@Override
public boolean start() {
long pathStateCheckInterval = PathStateCheckInterval.value();
diff --git a/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/CreateExtensionCmdTest.java b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/CreateExtensionCmdTest.java
index 2edb6ea48e3..2f630966056 100644
--- a/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/CreateExtensionCmdTest.java
+++ b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/CreateExtensionCmdTest.java
@@ -94,4 +94,18 @@ public class CreateExtensionCmdTest {
setField(cmd, "details", details);
assertTrue(MapUtils.isNotEmpty(cmd.getDetails()));
}
+
+ @Test
+ public void getReservedResourceDetailsReturnsValueWhenSet() {
+ setField(cmd, "reservedResourceDetails", "detail1,detail2,detail3");
+ String result = cmd.getReservedResourceDetails();
+ assertEquals("detail1,detail2,detail3", result);
+ }
+
+ @Test
+ public void getReservedResourceDetailsReturnsNullWhenNotSet() {
+ setField(cmd, "reservedResourceDetails", null);
+ String result = cmd.getReservedResourceDetails();
+ assertNull(result);
+ }
}
diff --git a/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/UpdateExtensionCmdTest.java b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/UpdateExtensionCmdTest.java
index f0a3a6fcf21..5c5c2014a52 100644
--- a/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/UpdateExtensionCmdTest.java
+++ b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/UpdateExtensionCmdTest.java
@@ -26,6 +26,7 @@ import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import static org.springframework.test.util.ReflectionTestUtils.setField;
import java.util.EnumSet;
import java.util.HashMap;
@@ -134,6 +135,20 @@ public class UpdateExtensionCmdTest {
assertTrue(cmd.isCleanupDetails());
}
+ @Test
+ public void getReservedResourceDetailsReturnsValueWhenSet() {
+ setField(cmd, "reservedResourceDetails", "detail1,detail2,detail3");
+ String result = cmd.getReservedResourceDetails();
+ assertEquals("detail1,detail2,detail3", result);
+ }
+
+ @Test
+ public void getReservedResourceDetailsReturnsNullWhenNotSet() {
+ setField(cmd, "reservedResourceDetails", null);
+ String result = cmd.getReservedResourceDetails();
+ assertNull(result);
+ }
+
@Test
public void executeSetsExtensionResponseWhenManagerSucceeds() {
Extension extension = mock(Extension.class);
diff --git a/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManagerImplTest.java b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManagerImplTest.java
index 085ae212b28..ff3fce06b00 100644
--- a/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManagerImplTest.java
+++ b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManagerImplTest.java
@@ -23,11 +23,13 @@ import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyBoolean;
import static org.mockito.Mockito.anyLong;
import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
@@ -40,6 +42,7 @@ import static org.mockito.Mockito.when;
import java.io.File;
import java.security.InvalidParameterException;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
@@ -49,8 +52,6 @@ import java.util.List;
import java.util.Map;
import java.util.UUID;
-import com.cloud.exception.PermissionDeniedException;
-import com.cloud.user.AccountService;
import org.apache.cloudstack.acl.Role;
import org.apache.cloudstack.acl.RoleService;
import org.apache.cloudstack.acl.RoleType;
@@ -85,9 +86,11 @@ import org.apache.cloudstack.framework.extensions.dao.ExtensionResourceMapDao;
import org.apache.cloudstack.framework.extensions.dao.ExtensionResourceMapDetailsDao;
import org.apache.cloudstack.framework.extensions.vo.ExtensionCustomActionDetailsVO;
import org.apache.cloudstack.framework.extensions.vo.ExtensionCustomActionVO;
+import org.apache.cloudstack.framework.extensions.vo.ExtensionDetailsVO;
import org.apache.cloudstack.framework.extensions.vo.ExtensionResourceMapVO;
import org.apache.cloudstack.framework.extensions.vo.ExtensionVO;
import org.apache.cloudstack.utils.identity.ManagementServerNode;
+import org.apache.commons.collections.CollectionUtils;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -113,6 +116,7 @@ import com.cloud.dc.dao.ClusterDao;
import com.cloud.exception.AgentUnavailableException;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.exception.OperationTimedoutException;
+import com.cloud.exception.PermissionDeniedException;
import com.cloud.host.Host;
import com.cloud.host.dao.HostDao;
import com.cloud.host.dao.HostDetailsDao;
@@ -122,6 +126,7 @@ import com.cloud.org.Cluster;
import com.cloud.serializer.GsonHelper;
import com.cloud.storage.dao.VMTemplateDao;
import com.cloud.user.Account;
+import com.cloud.user.AccountService;
import com.cloud.utils.Pair;
import com.cloud.utils.UuidUtils;
import com.cloud.utils.db.EntityManager;
@@ -664,6 +669,8 @@ public class ExtensionsManagerImplTest {
when(cmd.getPath()).thenReturn(null);
when(cmd.isOrchestratorRequiresPrepareVm()).thenReturn(null);
when(cmd.getState()).thenReturn(null);
+ String reservedResourceDetails = "abc,xyz";
+ when(cmd.getReservedResourceDetails()).thenReturn(reservedResourceDetails);
when(extensionDao.findByName("ext1")).thenReturn(null);
when(extensionDao.persist(any())).thenAnswer(inv -> {
ExtensionVO extensionVO = inv.getArgument(0);
@@ -671,11 +678,20 @@ public class ExtensionsManagerImplTest {
return extensionVO;
});
when(managementServerHostDao.listBy(any())).thenReturn(Collections.emptyList());
-
+ List<ExtensionDetailsVO> detailsList = new ArrayList<>();
+ doAnswer(inv -> {
+ List<ExtensionDetailsVO> detailsVO = inv.getArgument(0);
+ detailsList.addAll(detailsVO);
+ return null;
+ }).when(extensionDetailsDao).saveDetails(anyList());
Extension ext = extensionsManager.createExtension(cmd);
assertEquals("ext1", ext.getName());
verify(extensionDao).persist(any());
+ assertTrue(CollectionUtils.isNotEmpty(detailsList));
+ assertTrue(detailsList.stream()
+ .anyMatch(detail -> ApiConstants.RESERVED_RESOURCE_DETAILS.equals(detail.getName())
+ && reservedResourceDetails.equals(detail.getValue())));
}
@Test
@@ -938,14 +954,32 @@ public class ExtensionsManagerImplTest {
public void updateExtensionsDetails_SavesDetails_WhenDetailsProvided() {
long extensionId = 10L;
Map<String, String> details = Map.of("foo", "bar", "baz", "qux");
- extensionsManager.updateExtensionsDetails(false, details, null, extensionId);
+ extensionsManager.updateExtensionsDetails(false, details, null, null, extensionId);
verify(extensionDetailsDao).saveDetails(any());
}
+ @Test
+ public void updateExtensionsDetails_PersistReservedDetail_WhenProvided() {
+ long extensionId = 10L;
+ when(extensionDetailsDao.persist(any())).thenReturn(mock(ExtensionDetailsVO.class));
+ extensionsManager.updateExtensionsDetails(false, null, null, "abc,xyz", extensionId);
+ verify(extensionDetailsDao).persist(any());
+ }
+
+ @Test
+ public void updateExtensionsDetails_UpdateReservedDetail_WhenProvided() {
+ long extensionId = 10L;
+ when(extensionDetailsDao.findDetail(anyLong(), eq(ApiConstants.RESERVED_RESOURCE_DETAILS)))
+ .thenReturn(mock(ExtensionDetailsVO.class));
+ when(extensionDetailsDao.update(anyLong(), any())).thenReturn(true);
+ extensionsManager.updateExtensionsDetails(false, null, null, "abc,xyz", extensionId);
+ verify(extensionDetailsDao).update(anyLong(), any());
+ }
+
@Test
public void updateExtensionsDetails_DoesNothing_WhenDetailsAndCleanupAreNull() {
long extensionId = 11L;
- extensionsManager.updateExtensionsDetails(null, null, null, extensionId);
+ extensionsManager.updateExtensionsDetails(null, null, null, null, extensionId);
verify(extensionDetailsDao, never()).removeDetails(anyLong());
verify(extensionDetailsDao, never()).saveDetails(any());
}
@@ -953,7 +987,7 @@ public class ExtensionsManagerImplTest {
@Test
public void updateExtensionsDetails_RemovesDetailsOnly_WhenCleanupIsTrue() {
long extensionId = 12L;
- extensionsManager.updateExtensionsDetails(true, null, null, extensionId);
+ extensionsManager.updateExtensionsDetails(true, null, null, null, extensionId);
verify(extensionDetailsDao).removeDetails(extensionId);
verify(extensionDetailsDao, never()).saveDetails(any());
}
@@ -961,7 +995,7 @@ public class ExtensionsManagerImplTest {
@Test
public void updateExtensionsDetails_PersistsOrchestratorFlag_WhenFlagIsNotNull() {
long extensionId = 13L;
- extensionsManager.updateExtensionsDetails(false, null, true, extensionId);
+ extensionsManager.updateExtensionsDetails(false, null, true, null, extensionId);
verify(extensionDetailsDao).persist(any());
}
@@ -970,7 +1004,7 @@ public class ExtensionsManagerImplTest {
long extensionId = 14L;
Map<String, String> details = Map.of("foo", "bar");
doThrow(CloudRuntimeException.class).when(extensionDetailsDao).saveDetails(any());
- extensionsManager.updateExtensionsDetails(false, details, null, extensionId);
+ extensionsManager.updateExtensionsDetails(false, details, null, null, extensionId);
}
@Test
@@ -1161,7 +1195,8 @@ public class ExtensionsManagerImplTest {
when(externalProvisioner.getExtensionPath("entry2.sh")).thenReturn("/some/path/entry2.sh");
Map<String, String> hiddenDetails = Map.of(ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM, "false");
- when(extensionDetailsDao.listDetailsKeyPairs(2L, List.of(ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM)))
+ when(extensionDetailsDao.listDetailsKeyPairs(2L, List.of(
+ ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM, ApiConstants.RESERVED_RESOURCE_DETAILS)))
.thenReturn(hiddenDetails);
EnumSet<ApiConstants.ExtensionDetails> viewDetails = EnumSet.noneOf(ApiConstants.ExtensionDetails.class);
@@ -2069,4 +2104,118 @@ public class ExtensionsManagerImplTest {
}
}
+ @Test
+ public void getExtensionReservedResourceDetailsReturnsEmptyListWhenDetailsNotFound() {
+ long extensionId = 1L;
+ when(extensionDetailsDao.findDetail(extensionId, ApiConstants.RESERVED_RESOURCE_DETAILS)).thenReturn(null);
+
+ List<String> result = extensionsManager.getExtensionReservedResourceDetails(extensionId);
+
+ assertNotNull(result);
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ public void getExtensionReservedResourceDetailsReturnsEmptyListWhenValueIsBlank() {
+ long extensionId = 2L;
+ ExtensionDetailsVO detailsVO = mock(ExtensionDetailsVO.class);
+ when(detailsVO.getValue()).thenReturn(" ");
+ when(extensionDetailsDao.findDetail(extensionId, ApiConstants.RESERVED_RESOURCE_DETAILS)).thenReturn(detailsVO);
+
+ List<String> result = extensionsManager.getExtensionReservedResourceDetails(extensionId);
+
+ assertNotNull(result);
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ public void getExtensionReservedResourceDetailsReturnsListOfTrimmedDetails() {
+ long extensionId = 3L;
+ ExtensionDetailsVO detailsVO = mock(ExtensionDetailsVO.class);
+ when(detailsVO.getValue()).thenReturn(" detail1 , detail2,detail3 ");
+ when(extensionDetailsDao.findDetail(extensionId, ApiConstants.RESERVED_RESOURCE_DETAILS)).thenReturn(detailsVO);
+
+ List<String> result = extensionsManager.getExtensionReservedResourceDetails(extensionId);
+
+ assertNotNull(result);
+ assertEquals(3, result.size());
+ assertEquals("detail1", result.get(0));
+ assertEquals("detail2", result.get(1));
+ assertEquals("detail3", result.get(2));
+ }
+
+ @Test
+ public void getExtensionReservedResourceDetailsHandlesEmptyPartsGracefully() {
+ long extensionId = 4L;
+ ExtensionDetailsVO detailsVO = mock(ExtensionDetailsVO.class);
+ when(detailsVO.getValue()).thenReturn("detail1,,detail2, ,detail3");
+ when(extensionDetailsDao.findDetail(extensionId, ApiConstants.RESERVED_RESOURCE_DETAILS)).thenReturn(detailsVO);
+
+ List<String> result = extensionsManager.getExtensionReservedResourceDetails(extensionId);
+
+ assertNotNull(result);
+ assertEquals(3, result.size());
+ assertEquals("detail1", result.get(0));
+ assertEquals("detail2", result.get(1));
+ assertEquals("detail3", result.get(2));
+ }
+
+ @Test
+ public void getExtensionReservedResourceDetailsReturnsEmptyListWhenSplitResultsInNoParts() {
+ long extensionId = 5L;
+ ExtensionDetailsVO detailsVO = mock(ExtensionDetailsVO.class);
+ when(detailsVO.getValue()).thenReturn(",");
+ when(extensionDetailsDao.findDetail(extensionId, ApiConstants.RESERVED_RESOURCE_DETAILS)).thenReturn(detailsVO);
+
+ List<String> result = extensionsManager.getExtensionReservedResourceDetails(extensionId);
+
+ assertNotNull(result);
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ public void addInbuiltExtensionReservedResourceDetailsDoesNothingWhenExtensionNotFound() {
+ when(extensionDao.findById(1L)).thenReturn(null);
+ List<String> reservedResourceDetails = new ArrayList<>();
+ extensionsManager.addInbuiltExtensionReservedResourceDetails(1L, reservedResourceDetails);
+ assertTrue(reservedResourceDetails.isEmpty());
+ }
+
+ @Test
+ public void addInbuiltExtensionReservedResourceDetailsDoesNothingForUserDefinedExtension() {
+ ExtensionVO extension = mock(ExtensionVO.class);
+ when(extension.isUserDefined()).thenReturn(true);
+ when(extensionDao.findById(2L)).thenReturn(extension);
+ List<String> reservedResourceDetails = new ArrayList<>();
+ reservedResourceDetails.add("existing-detail");
+ extensionsManager.addInbuiltExtensionReservedResourceDetails(2L, reservedResourceDetails);
+ assertEquals(1, reservedResourceDetails.size());
+ assertTrue(reservedResourceDetails.contains("existing-detail"));
+ }
+
+ @Test
+ public void addInbuiltExtensionReservedResourceDetailsDoesNothingWhenNoMatchFound() {
+ ExtensionVO extension = mock(ExtensionVO.class);
+ when(extension.isUserDefined()).thenReturn(false);
+ when(extension.getName()).thenReturn("no-such-inbuilt-key-expected");
+ when(extensionDao.findById(3L)).thenReturn(extension);
+ List<String> reservedResourceDetails = new ArrayList<>();
+ extensionsManager.addInbuiltExtensionReservedResourceDetails(3L, reservedResourceDetails);
+ assertTrue(reservedResourceDetails.isEmpty());
+ }
+
+ @Test
+ public void addInbuiltExtensionReservedResourceDetailsAddedDetails() {
+ ExtensionVO extension = mock(ExtensionVO.class);
+ when(extension.isUserDefined()).thenReturn(false);
+ Map.Entry<String, List<String>> entry =
+ ExtensionsManagerImpl.INBUILT_RESERVED_RESOURCE_DETAILS.entrySet().iterator().next();
+ when(extension.getName()).thenReturn(entry.getKey());
+ when(extensionDao.findById(3L)).thenReturn(extension);
+ List<String> reservedResourceDetails = new ArrayList<>();
+ extensionsManager.addInbuiltExtensionReservedResourceDetails(3L, reservedResourceDetails);
+ assertFalse(reservedResourceDetails.isEmpty());
+ assertEquals(reservedResourceDetails.size(), entry.getValue().size());
+ assertTrue(reservedResourceDetails.containsAll(entry.getValue()));
+ }
}
diff --git a/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java
index a2f9544de39..84b259a89a0 100644
--- a/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java
+++ b/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java
@@ -17,14 +17,13 @@
package com.cloud.api.query.dao;
import java.text.DecimalFormat;
-import java.util.ArrayList;
-import java.util.Collections;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
import java.util.Calendar;
+import java.util.Collections;
import java.util.Date;
-
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
@@ -34,8 +33,6 @@ import java.util.stream.Collectors;
import javax.inject.Inject;
-import com.cloud.gpu.dao.VgpuProfileDao;
-import com.cloud.service.dao.ServiceOfferingDao;
import org.apache.cloudstack.affinity.AffinityGroupResponse;
import org.apache.cloudstack.annotation.AnnotationService;
import org.apache.cloudstack.annotation.dao.AnnotationDao;
@@ -49,6 +46,7 @@ import org.apache.cloudstack.api.response.SecurityGroupResponse;
import org.apache.cloudstack.api.response.UserVmResponse;
import org.apache.cloudstack.api.response.VnfNicResponse;
import org.apache.cloudstack.context.CallContext;
+import org.apache.cloudstack.extension.ExtensionHelper;
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
import org.apache.cloudstack.query.QueryService;
import org.apache.cloudstack.vm.lease.VMLeaseManager;
@@ -61,11 +59,13 @@ import com.cloud.api.ApiDBUtils;
import com.cloud.api.ApiResponseHelper;
import com.cloud.api.query.vo.UserVmJoinVO;
import com.cloud.gpu.GPU;
+import com.cloud.gpu.dao.VgpuProfileDao;
import com.cloud.host.ControlState;
import com.cloud.network.IpAddress;
import com.cloud.network.vpc.VpcVO;
import com.cloud.network.vpc.dao.VpcDao;
import com.cloud.service.ServiceOfferingDetailsVO;
+import com.cloud.service.dao.ServiceOfferingDao;
import com.cloud.storage.DiskOfferingVO;
import com.cloud.storage.GuestOS;
import com.cloud.storage.Storage.TemplateType;
@@ -92,7 +92,6 @@ import com.cloud.vm.VirtualMachine.State;
import com.cloud.vm.VmStats;
import com.cloud.vm.dao.NicExtraDhcpOptionDao;
import com.cloud.vm.dao.NicSecondaryIpVO;
-
import com.cloud.vm.dao.VMInstanceDetailsDao;
@Component
@@ -124,6 +123,8 @@ public class UserVmJoinDaoImpl extends GenericDaoBaseWithTagInformation<UserVmJo
private ServiceOfferingDao serviceOfferingDao;
@Inject
private VgpuProfileDao vgpuProfileDao;
+ @Inject
+ ExtensionHelper extensionHelper;
private final SearchBuilder<UserVmJoinVO> VmDetailSearch;
private final SearchBuilder<UserVmJoinVO> activeVmByIsoSearch;
@@ -456,7 +457,16 @@ public class UserVmJoinDaoImpl extends GenericDaoBaseWithTagInformation<UserVmJo
// Remove deny listed settings if user is not admin
if (caller.getType() != Account.Type.ADMIN) {
- String[] userVmSettingsToHide = QueryService.UserVMDeniedDetails.value().split(",");
+ List<String> userVmSettingsToHide = new ArrayList<>();
+ String[] parts = QueryService.UserVMDeniedDetails.value().split(",");
+ if (parts.length > 0) {
+ Collections.addAll(userVmSettingsToHide, parts);
+ }
+ if (userVm.getTemplateExtensionId() != null) {
+ userVmSettingsToHide.addAll(extensionHelper.getExtensionReservedResourceDetails(
+ userVm.getTemplateExtensionId()));
+ }
+
for (String key : userVmSettingsToHide) {
resourceDetails.remove(key.trim());
}
@@ -512,7 +522,6 @@ public class UserVmJoinDaoImpl extends GenericDaoBaseWithTagInformation<UserVmJo
return userVmResponse;
}
-
private long computeLeaseDurationFromExpiryDate(Date created, Date leaseExpiryDate) {
LocalDate createdDate = created.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
LocalDate expiryDate = leaseExpiryDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
diff --git a/server/src/main/java/com/cloud/api/query/vo/UserVmJoinVO.java b/server/src/main/java/com/cloud/api/query/vo/UserVmJoinVO.java
index eab34081d51..d5677ef032d 100644
--- a/server/src/main/java/com/cloud/api/query/vo/UserVmJoinVO.java
+++ b/server/src/main/java/com/cloud/api/query/vo/UserVmJoinVO.java
@@ -207,6 +207,9 @@ public class UserVmJoinVO extends BaseViewWithTagInformationVO implements Contro
@Column(name = "template_format")
private Storage.ImageFormat templateFormat;
+ @Column(name = "template_extension_id")
+ private Long templateExtensionId;
+
@Column(name = "password_enabled")
private boolean passwordEnabled;
@@ -709,6 +712,10 @@ public class UserVmJoinVO extends BaseViewWithTagInformationVO implements Contro
return templateFormat;
}
+ public Long getTemplateExtensionId() {
+ return templateExtensionId;
+ }
+
public boolean isPasswordEnabled() {
return passwordEnabled;
}
diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java
index 24baf538394..01f8558658e 100644
--- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java
+++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java
@@ -126,6 +126,7 @@ import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory;
import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo;
import org.apache.cloudstack.engine.subsystem.api.storage.VolumeService;
import org.apache.cloudstack.engine.subsystem.api.storage.VolumeService.VolumeApiResult;
+import org.apache.cloudstack.extension.ExtensionHelper;
import org.apache.cloudstack.framework.async.AsyncCallFuture;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.framework.config.Configurable;
@@ -632,6 +633,9 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
@Inject
SnapshotDataFactory snapshotDataFactory;
+ @Inject
+ ExtensionHelper extensionHelper;
+
private ScheduledExecutorService _executor = null;
private ScheduledExecutorService _vmIpFetchExecutor = null;
private int _expungeInterval;
@@ -2907,6 +2911,11 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
.map(item -> (item).trim())
.collect(Collectors.toList());
userDenyListedSettings.addAll(QueryService.RootAdminOnlyVmSettings);
+ if (template != null && template.getExtensionId() != null) {
+ userDenyListedSettings.addAll(extensionHelper.getExtensionReservedResourceDetails(
+ template.getExtensionId()));
+ }
+
final List<String> userReadOnlySettings = Stream.of(QueryService.UserVMReadOnlyDetails.value().split(","))
.map(item -> (item).trim())
.collect(Collectors.toList());
diff --git a/server/src/test/java/com/cloud/api/query/dao/UserVmJoinDaoImplTest.java b/server/src/test/java/com/cloud/api/query/dao/UserVmJoinDaoImplTest.java
index 14074add021..7dc3a486779 100755
--- a/server/src/test/java/com/cloud/api/query/dao/UserVmJoinDaoImplTest.java
+++ b/server/src/test/java/com/cloud/api/query/dao/UserVmJoinDaoImplTest.java
@@ -26,6 +26,7 @@ import org.apache.cloudstack.annotation.dao.AnnotationDao;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.ResponseObject;
import org.apache.cloudstack.api.response.UserVmResponse;
+import org.apache.cloudstack.extension.ExtensionHelper;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
@@ -78,6 +79,9 @@ public class UserVmJoinDaoImplTest extends GenericDaoBaseWithTagInformationBaseT
@Mock
private VnfTemplateDetailsDao vnfTemplateDetailsDao;
+ @Mock
+ ExtensionHelper extensionHelper;
+
private UserVmJoinVO userVm = new UserVmJoinVO();
private UserVmResponse userVmResponse = new UserVmResponse();
diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json
index 4f450e940fc..00078c0a2b5 100644
--- a/ui/public/locales/en.json
+++ b/ui/public/locales/en.json
@@ -2087,6 +2087,7 @@
"label.requireshvm": "HVM",
"label.requiresupgrade": "Requires upgrade",
"label.reserved": "Reserved",
+"label.reservedresourcedetails": "Reserved resource details",
"label.reserved.system.gateway": "Reserved system gateway",
"label.reserved.system.ip": "Reserved system IP",
"label.reserved.system.netmask": "Reserved system netmask",
diff --git a/ui/src/config/section/extension.js b/ui/src/config/section/extension.js
index 4c6d9ebf076..5904abae30b 100644
--- a/ui/src/config/section/extension.js
+++ b/ui/src/config/section/extension.js
@@ -44,7 +44,7 @@ export default {
}, 'created']
return fields
},
- details: ['name', 'description', 'id', 'type', 'details', 'path', 'pathready', 'isuserdefined', 'orchestratorrequirespreparevm', 'created'],
+ details: ['name', 'description', 'id', 'type', 'details', 'path', 'pathready', 'isuserdefined', 'orchestratorrequirespreparevm', 'reservedresourcedetails', 'created'],
filters: ['orchestrator'],
tabs: [{
name: 'details',
diff --git a/ui/src/views/extension/CreateExtension.vue b/ui/src/views/extension/CreateExtension.vue
index 9a7e0c8ae76..6cbd934a1f2 100644
--- a/ui/src/views/extension/CreateExtension.vue
+++ b/ui/src/views/extension/CreateExtension.vue
@@ -89,6 +89,14 @@
<details-input
v-model:value="form.details" />
</a-form-item>
+ <a-form-item name="reservedresourcedetails" ref="reservedresourcedetails">
+ <template #label>
+ <tooltip-label :title="$t('label.reservedresourcedetails')" :tooltip="apiParams.reservedresourcedetails.description"/>
+ </template>
+ <a-input
+ v-model:value="form.reservedresourcedetails"
+ :placeholder="apiParams.reservedresourcedetails.description" />
+ </a-form-item>
<a-form-item name="state" ref="state">
<template #label>
<tooltip-label :title="$t('label.enabled')" :tooltip="apiParams.state.description"/>
@@ -201,6 +209,9 @@ export default {
params['details[0].' + key] = value
})
}
+ if (values.reservedresourcedetails) {
+ params.reservedresourcedetails = values.reservedresourcedetails
+ }
postAPI('createExtension', params).then(response => {
this.$emit('refresh-data')
this.$notification.success({
diff --git a/ui/src/views/extension/UpdateExtension.vue b/ui/src/views/extension/UpdateExtension.vue
index 192a339b43a..0926c6268b6 100644
--- a/ui/src/views/extension/UpdateExtension.vue
+++ b/ui/src/views/extension/UpdateExtension.vue
@@ -46,6 +46,14 @@
<details-input
v-model:value="form.details" />
</a-form-item>
+ <a-form-item name="reservedresourcedetails" ref="reservedresourcedetails">
+ <template #label>
+ <tooltip-label :title="$t('label.reservedresourcedetails')" :tooltip="apiParams.reservedresourcedetails.description"/>
+ </template>
+ <a-input
+ v-model:value="form.reservedresourcedetails"
+ :placeholder="apiParams.reservedresourcedetails.description" />
+ </a-form-item>
<div :span="24" class="action-button">
<a-button @click="closeAction">{{ $t('label.cancel') }}</a-button>
<a-button :loading="loading" ref="submit" type="primary" @click="handleSubmit">{{ $t('label.ok') }}</a-button>
@@ -90,13 +98,16 @@ export default {
this.form = reactive({
description: this.resource.description,
details: this.resource.details,
- orchestratorrequirespreparevm: this.resource.orchestratorrequirespreparevm
+ orchestratorrequirespreparevm: this.resource.orchestratorrequirespreparevm,
+ reservedresourcedetails: this.resource.reservedresourcedetails
})
},
fetchData () {
this.loading = true
getAPI('listExtensions', { id: this.resource.id }).then(json => {
- this.form.details = json?.listextensionsresponse?.extension?.[0]?.details
+ const ext = json?.listextensionsresponse?.extension?.[0] || {}
+ this.form.details = ext.details
+ this.form.reservedresourcedetails = ext.reservedresourcedetails
}).finally(() => {
this.loading = false
})
@@ -110,7 +121,7 @@ export default {
const params = {
id: this.resource.id
}
- const keys = ['description', 'orchestratorrequirespreparevm']
+ const keys = ['description', 'orchestratorrequirespreparevm', 'reservedresourcedetails']
for (const key of keys) {
if (values[key] !== undefined || values[key] !== null) {
params[key] = values[key]