Skip to content

Commit 95816b4

Browse files
shwstpprDaan Hoogland
authored andcommitted
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 <abhishek.mrt22@gmail.com>
1 parent d11d182 commit 95816b4

18 files changed

Lines changed: 371 additions & 34 deletions

File tree

api/src/main/java/org/apache/cloudstack/api/ApiConstants.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,7 @@ public class ApiConstants {
502502
public static final String RECOVER = "recover";
503503
public static final String REPAIR = "repair";
504504
public static final String REQUIRES_HVM = "requireshvm";
505+
public static final String RESERVED_RESOURCE_DETAILS = "reservedresourcedetails";
505506
public static final String RESOURCES = "resources";
506507
public static final String RESOURCE_COUNT = "resourcecount";
507508
public static final String RESOURCE_NAME = "resourcename";

api/src/main/java/org/apache/cloudstack/api/response/ExtensionResponse.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,12 @@ public class ExtensionResponse extends BaseResponse {
8585
@Param(description = "Removal timestamp of the extension, if applicable")
8686
private Date removed;
8787

88+
@SerializedName(ApiConstants.RESERVED_RESOURCE_DETAILS)
89+
@Param(description = "Resource detail names as comma separated string that should be reserved and not visible " +
90+
"to end users",
91+
since = "4.22.1")
92+
protected String reservedResourceDetails;
93+
8894
public ExtensionResponse(String id, String name, String description, String type) {
8995
this.id = id;
9096
this.name = name;
@@ -179,4 +185,8 @@ public Date getRemoved() {
179185
public void setRemoved(Date removed) {
180186
this.removed = removed;
181187
}
188+
189+
public void setReservedResourceDetails(String reservedResourceDetails) {
190+
this.reservedResourceDetails = reservedResourceDetails;
191+
}
182192
}

api/src/main/java/org/apache/cloudstack/extension/ExtensionHelper.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@
1717

1818
package org.apache.cloudstack.extension;
1919

20+
import java.util.List;
21+
2022
public interface ExtensionHelper {
2123
Long getExtensionIdForCluster(long clusterId);
2224
Extension getExtension(long id);
2325
Extension getExtensionForCluster(long clusterId);
26+
List<String> getExtensionReservedResourceDetails(long extensionId);
2427
}

engine/schema/src/main/resources/META-INF/db/views/cloud.user_vm_view.sql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ SELECT
7979
`vm_template`.`format` AS `template_format`,
8080
`vm_template`.`display_text` AS `template_display_text`,
8181
`vm_template`.`enable_password` AS `password_enabled`,
82+
`vm_template`.`extension_id` AS `template_extension_id`,
8283
`iso`.`id` AS `iso_id`,
8384
`iso`.`uuid` AS `iso_uuid`,
8485
`iso`.`name` AS `iso_name`,

framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/CreateExtensionCmd.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,12 @@ public class CreateExtensionCmd extends BaseCmd {
8383
description = "Details in key/value pairs using format details[i].keyname=keyvalue. Example: details[0].endpoint.url=urlvalue")
8484
protected Map details;
8585

86+
@Parameter(name = ApiConstants.RESERVED_RESOURCE_DETAILS, type = CommandType.STRING,
87+
description = "Resource detail names as comma separated string that should be reserved and not visible " +
88+
"to end users",
89+
since = "4.22.1")
90+
protected String reservedResourceDetails;
91+
8692
/////////////////////////////////////////////////////
8793
/////////////////// Accessors ///////////////////////
8894
/////////////////////////////////////////////////////
@@ -115,6 +121,10 @@ public Map<String, String> getDetails() {
115121
return convertDetailsToMap(details);
116122
}
117123

124+
public String getReservedResourceDetails() {
125+
return reservedResourceDetails;
126+
}
127+
118128
/////////////////////////////////////////////////////
119129
/////////////// API Implementation///////////////////
120130
/////////////////////////////////////////////////////

framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/UpdateExtensionCmd.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,12 @@ public class UpdateExtensionCmd extends BaseCmd {
7878
"if false or not set, no action)")
7979
private Boolean cleanupDetails;
8080

81+
@Parameter(name = ApiConstants.RESERVED_RESOURCE_DETAILS, type = CommandType.STRING,
82+
description = "Resource detail names as comma separated string that should be reserved and not visible " +
83+
"to end users",
84+
since = "4.22.1")
85+
protected String reservedResourceDetails;
86+
8187
/////////////////////////////////////////////////////
8288
/////////////////// Accessors ///////////////////////
8389
/////////////////////////////////////////////////////
@@ -106,6 +112,10 @@ public Boolean isCleanupDetails() {
106112
return cleanupDetails;
107113
}
108114

115+
public String getReservedResourceDetails() {
116+
return reservedResourceDetails;
117+
}
118+
109119
/////////////////////////////////////////////////////
110120
/////////////// API Implementation///////////////////
111121
/////////////////////////////////////////////////////

framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManagerImpl.java

Lines changed: 85 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,11 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
216216
@Inject
217217
AccountService accountService;
218218

219+
// Map of in-built extension names and their reserved resource details that shouldn't be accessible to end-users
220+
protected static final Map<String, List<String>> INBUILT_RESERVED_RESOURCE_DETAILS = Map.of(
221+
"proxmox", List.of("proxmox_vmid")
222+
);
223+
219224
private ScheduledExecutorService extensionPathStateCheckExecutor;
220225

221226
protected String getDefaultExtensionRelativePath(String name) {
@@ -563,6 +568,25 @@ protected void checkExtensionPathState(Extension extension, List<ManagementServe
563568
updateExtensionPathReady(extension, true);
564569
}
565570

571+
protected void addInbuiltExtensionReservedResourceDetails(long extensionId, List<String> reservedResourceDetails) {
572+
ExtensionVO vo = extensionDao.findById(extensionId);
573+
if (vo == null || vo.isUserDefined()) {
574+
return;
575+
}
576+
String lowerName = StringUtils.defaultString(vo.getName()).toLowerCase();
577+
Optional<Map.Entry<String, List<String>>> match = INBUILT_RESERVED_RESOURCE_DETAILS.entrySet().stream()
578+
.filter(e -> lowerName.contains(e.getKey().toLowerCase()))
579+
.findFirst();
580+
if (match.isPresent()) {
581+
Set<String> existing = new HashSet<>(reservedResourceDetails);
582+
for (String detailKey : match.get().getValue()) {
583+
if (existing.add(detailKey)) {
584+
reservedResourceDetails.add(detailKey);
585+
}
586+
}
587+
}
588+
}
589+
566590
@Override
567591
public String getExtensionsPath() {
568592
return externalProvisioner.getExtensionsPath();
@@ -577,6 +601,7 @@ public Extension createExtension(CreateExtensionCmd cmd) {
577601
String relativePath = cmd.getPath();
578602
final Boolean orchestratorRequiresPrepareVm = cmd.isOrchestratorRequiresPrepareVm();
579603
final String stateStr = cmd.getState();
604+
final String reservedResourceDetails = cmd.getReservedResourceDetails();
580605
ExtensionVO extensionByName = extensionDao.findByName(name);
581606
if (extensionByName != null) {
582607
throw new CloudRuntimeException("Extension by name already exists");
@@ -624,6 +649,10 @@ public Extension createExtension(CreateExtensionCmd cmd) {
624649
ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM, String.valueOf(orchestratorRequiresPrepareVm),
625650
false));
626651
}
652+
if (StringUtils.isNotBlank(reservedResourceDetails)) {
653+
detailsVOList.add(new ExtensionDetailsVO(extension.getId(),
654+
ApiConstants.RESERVED_RESOURCE_DETAILS, reservedResourceDetails, false));
655+
}
627656
if (CollectionUtils.isNotEmpty(detailsVOList)) {
628657
extensionDetailsDao.saveDetails(detailsVOList);
629658
}
@@ -704,6 +733,7 @@ public Extension updateExtension(UpdateExtensionCmd cmd) {
704733
final String stateStr = cmd.getState();
705734
final Map<String, String> details = cmd.getDetails();
706735
final Boolean cleanupDetails = cmd.isCleanupDetails();
736+
final String reservedResourceDetails = cmd.getReservedResourceDetails();
707737
final ExtensionVO extensionVO = extensionDao.findById(id);
708738
if (extensionVO == null) {
709739
throw new InvalidParameterValueException("Failed to find the extension");
@@ -732,7 +762,8 @@ public Extension updateExtension(UpdateExtensionCmd cmd) {
732762
throw new CloudRuntimeException(String.format("Failed to updated the extension: %s",
733763
extensionVO.getName()));
734764
}
735-
updateExtensionsDetails(cleanupDetails, details, orchestratorRequiresPrepareVm, id);
765+
updateExtensionsDetails(cleanupDetails, details, orchestratorRequiresPrepareVm, reservedResourceDetails,
766+
id);
736767
return extensionVO;
737768
});
738769
if (StringUtils.isNotBlank(stateStr)) {
@@ -748,9 +779,11 @@ public Extension updateExtension(UpdateExtensionCmd cmd) {
748779
return result;
749780
}
750781

751-
protected void updateExtensionsDetails(Boolean cleanupDetails, Map<String, String> details, Boolean orchestratorRequiresPrepareVm, long id) {
782+
protected void updateExtensionsDetails(Boolean cleanupDetails, Map<String, String> details,
783+
Boolean orchestratorRequiresPrepareVm, String reservedResourceDetails, long id) {
752784
final boolean needToUpdateAllDetails = Boolean.TRUE.equals(cleanupDetails) || MapUtils.isNotEmpty(details);
753-
if (!needToUpdateAllDetails && orchestratorRequiresPrepareVm == null) {
785+
if (!needToUpdateAllDetails && orchestratorRequiresPrepareVm == null &&
786+
StringUtils.isBlank(reservedResourceDetails)) {
754787
return;
755788
}
756789
if (needToUpdateAllDetails) {
@@ -761,6 +794,9 @@ protected void updateExtensionsDetails(Boolean cleanupDetails, Map<String, Strin
761794
hiddenDetails.put(ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM,
762795
String.valueOf(orchestratorRequiresPrepareVm));
763796
}
797+
if (StringUtils.isNotBlank(reservedResourceDetails)) {
798+
hiddenDetails.put(ApiConstants.RESERVED_RESOURCE_DETAILS, reservedResourceDetails);
799+
}
764800
if (MapUtils.isNotEmpty(hiddenDetails)) {
765801
hiddenDetails.forEach((key, value) -> detailsVOList.add(
766802
new ExtensionDetailsVO(id, key, value, false)));
@@ -775,15 +811,29 @@ protected void updateExtensionsDetails(Boolean cleanupDetails, Map<String, Strin
775811
extensionDetailsDao.removeDetails(id);
776812
}
777813
} else {
778-
ExtensionDetailsVO detailsVO = extensionDetailsDao.findDetail(id,
779-
ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM);
780-
if (detailsVO == null) {
781-
extensionDetailsDao.persist(new ExtensionDetailsVO(id,
782-
ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM,
783-
String.valueOf(orchestratorRequiresPrepareVm), false));
784-
} else if (Boolean.parseBoolean(detailsVO.getValue()) != orchestratorRequiresPrepareVm) {
785-
detailsVO.setValue(String.valueOf(orchestratorRequiresPrepareVm));
786-
extensionDetailsDao.update(detailsVO.getId(), detailsVO);
814+
if (orchestratorRequiresPrepareVm != null) {
815+
ExtensionDetailsVO detailsVO = extensionDetailsDao.findDetail(id,
816+
ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM);
817+
if (detailsVO == null) {
818+
extensionDetailsDao.persist(new ExtensionDetailsVO(id,
819+
ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM,
820+
String.valueOf(orchestratorRequiresPrepareVm), false));
821+
} else if (Boolean.parseBoolean(detailsVO.getValue()) != orchestratorRequiresPrepareVm) {
822+
detailsVO.setValue(String.valueOf(orchestratorRequiresPrepareVm));
823+
extensionDetailsDao.update(detailsVO.getId(), detailsVO);
824+
}
825+
}
826+
if (StringUtils.isNotBlank(reservedResourceDetails)) {
827+
ExtensionDetailsVO detailsVO = extensionDetailsDao.findDetail(id,
828+
ApiConstants.RESERVED_RESOURCE_DETAILS);
829+
if (detailsVO == null) {
830+
extensionDetailsDao.persist(new ExtensionDetailsVO(id,
831+
ApiConstants.RESERVED_RESOURCE_DETAILS,
832+
reservedResourceDetails, false));
833+
} else if (!reservedResourceDetails.equals(detailsVO.getValue())) {
834+
detailsVO.setValue(reservedResourceDetails);
835+
extensionDetailsDao.update(detailsVO.getId(), detailsVO);
836+
}
787837
}
788838
}
789839
}
@@ -961,12 +1011,16 @@ public ExtensionResponse createExtensionResponse(Extension extension,
9611011
hiddenDetails = extensionDetails.second();
9621012
} else {
9631013
hiddenDetails = extensionDetailsDao.listDetailsKeyPairs(extension.getId(),
964-
List.of(ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM));
1014+
List.of(ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM,
1015+
ApiConstants.RESERVED_RESOURCE_DETAILS));
9651016
}
9661017
if (hiddenDetails.containsKey(ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM)) {
9671018
response.setOrchestratorRequiresPrepareVm(Boolean.parseBoolean(
9681019
hiddenDetails.get(ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM)));
9691020
}
1021+
if (hiddenDetails.containsKey(ApiConstants.RESERVED_RESOURCE_DETAILS)) {
1022+
response.setReservedResourceDetails(hiddenDetails.get(ApiConstants.RESERVED_RESOURCE_DETAILS));
1023+
}
9701024
response.setObjectName(Extension.class.getSimpleName().toLowerCase());
9711025
return response;
9721026
}
@@ -1605,6 +1659,24 @@ public Extension getExtensionForCluster(long clusterId) {
16051659
return extensionDao.findById(extensionId);
16061660
}
16071661

1662+
@Override
1663+
public List<String> getExtensionReservedResourceDetails(long extensionId) {
1664+
ExtensionDetailsVO detailsVO = extensionDetailsDao.findDetail(extensionId,
1665+
ApiConstants.RESERVED_RESOURCE_DETAILS);
1666+
if (detailsVO == null || !StringUtils.isNotBlank(detailsVO.getValue())) {
1667+
return Collections.emptyList();
1668+
}
1669+
List<String> reservedDetails = new ArrayList<>();
1670+
String[] parts = detailsVO.getValue().split(",");
1671+
for (String part : parts) {
1672+
if (StringUtils.isNotBlank(part)) {
1673+
reservedDetails.add(part.trim());
1674+
}
1675+
}
1676+
addInbuiltExtensionReservedResourceDetails(extensionId, reservedDetails);
1677+
return reservedDetails;
1678+
}
1679+
16081680
@Override
16091681
public boolean start() {
16101682
long pathStateCheckInterval = PathStateCheckInterval.value();

framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/CreateExtensionCmdTest.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,4 +94,18 @@ public void testGetDetailsReturnsMap() {
9494
setField(cmd, "details", details);
9595
assertTrue(MapUtils.isNotEmpty(cmd.getDetails()));
9696
}
97+
98+
@Test
99+
public void getReservedResourceDetailsReturnsValueWhenSet() {
100+
setField(cmd, "reservedResourceDetails", "detail1,detail2,detail3");
101+
String result = cmd.getReservedResourceDetails();
102+
assertEquals("detail1,detail2,detail3", result);
103+
}
104+
105+
@Test
106+
public void getReservedResourceDetailsReturnsNullWhenNotSet() {
107+
setField(cmd, "reservedResourceDetails", null);
108+
String result = cmd.getReservedResourceDetails();
109+
assertNull(result);
110+
}
97111
}

framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/UpdateExtensionCmdTest.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import static org.mockito.Mockito.mock;
2727
import static org.mockito.Mockito.verify;
2828
import static org.mockito.Mockito.when;
29+
import static org.springframework.test.util.ReflectionTestUtils.setField;
2930

3031
import java.util.EnumSet;
3132
import java.util.HashMap;
@@ -134,6 +135,20 @@ public void isCleanupDetailsReturnsValueWhenSet() {
134135
assertTrue(cmd.isCleanupDetails());
135136
}
136137

138+
@Test
139+
public void getReservedResourceDetailsReturnsValueWhenSet() {
140+
setField(cmd, "reservedResourceDetails", "detail1,detail2,detail3");
141+
String result = cmd.getReservedResourceDetails();
142+
assertEquals("detail1,detail2,detail3", result);
143+
}
144+
145+
@Test
146+
public void getReservedResourceDetailsReturnsNullWhenNotSet() {
147+
setField(cmd, "reservedResourceDetails", null);
148+
String result = cmd.getReservedResourceDetails();
149+
assertNull(result);
150+
}
151+
137152
@Test
138153
public void executeSetsExtensionResponseWhenManagerSucceeds() {
139154
Extension extension = mock(Extension.class);

0 commit comments

Comments
 (0)