This is an automated email from the ASF dual-hosted git repository.
sureshanaparti pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cloudstack.git
The following commit(s) were added to refs/heads/main by this push:
new 4aed972e78c api,server,extensions: allow updating extension resource
map details (#11303)
4aed972e78c is described below
commit 4aed972e78c44674a2f65683f2f4434bd39de979
Author: Abhishek Kumar <[email protected]>
AuthorDate: Tue Jul 29 10:25:52 2025 +0530
api,server,extensions: allow updating extension resource map details
(#11303)
* api,server,extensions: allow updating extension resource map details
This PR makes changes for allowing updating details for an extension
resource mapping.
Currently, extensions only support Cluster to be registered therefore
changes has been added to updateCluster functionality.
Signed-off-by: Abhishek Kumar <[email protected]>
---
.../command/admin/cluster/UpdateClusterCmd.java | 12 ++++
.../extensions/manager/ExtensionsManager.java | 6 ++
.../extensions/manager/ExtensionsManagerImpl.java | 39 +++++++++++
.../manager/ExtensionsManagerImplTest.java | 76 ++++++++++++++++++++++
.../com/cloud/resource/ResourceManagerImpl.java | 17 ++++-
.../extension/ExternalConfigurationDetails.vue | 3 +-
ui/src/views/infra/ClusterUpdate.vue | 41 +++++++++++-
7 files changed, 190 insertions(+), 4 deletions(-)
diff --git
a/api/src/main/java/org/apache/cloudstack/api/command/admin/cluster/UpdateClusterCmd.java
b/api/src/main/java/org/apache/cloudstack/api/command/admin/cluster/UpdateClusterCmd.java
index 816285e3430..c160cfd2e03 100644
---
a/api/src/main/java/org/apache/cloudstack/api/command/admin/cluster/UpdateClusterCmd.java
+++
b/api/src/main/java/org/apache/cloudstack/api/command/admin/cluster/UpdateClusterCmd.java
@@ -16,6 +16,8 @@
// under the License.
package org.apache.cloudstack.api.command.admin.cluster;
+import java.util.Map;
+
import com.cloud.cpu.CPU;
import org.apache.cloudstack.api.ApiCommandResourceType;
@@ -60,6 +62,12 @@ public class UpdateClusterCmd extends BaseCmd {
since = "4.20")
private String arch;
+ @Parameter(name = ApiConstants.EXTERNAL_DETAILS,
+ type = CommandType.MAP,
+ description = "Details in key/value pairs to be added to the
extension-resource mapping. Use the format externaldetails[i].<key>=<value>.
Example: externaldetails[0].endpoint.url=https://example.com",
+ since = "4.21.0")
+ protected Map externalDetails;
+
public String getClusterName() {
return clusterName;
}
@@ -122,6 +130,10 @@ public class UpdateClusterCmd extends BaseCmd {
return CPU.CPUArch.fromType(arch);
}
+ public Map<String, String> getExternalDetails() {
+ return convertDetailsToMap(externalDetails);
+ }
+
@Override
public void execute() {
Cluster cluster = _resourceService.getCluster(getId());
diff --git
a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManager.java
b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManager.java
index 8b9ad96b3c4..82174872e87 100644
---
a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManager.java
+++
b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManager.java
@@ -46,6 +46,7 @@ import
org.apache.cloudstack.framework.extensions.command.ExtensionServerActionB
import com.cloud.host.Host;
import com.cloud.org.Cluster;
+import com.cloud.utils.Pair;
import com.cloud.utils.component.Manager;
public interface ExtensionsManager extends Manager {
@@ -87,4 +88,9 @@ public interface ExtensionsManager extends Manager {
Map<String, Map<String, String>> getExternalAccessDetails(Host host,
Map<String, String> vmDetails);
String handleExtensionServerCommands(ExtensionServerActionBaseCommand cmd);
+
+ Pair<Boolean, ExtensionResourceMap>
extensionResourceMapDetailsNeedUpdate(final long resourceId,
+ final ExtensionResourceMap.ResourceType resourceType,
final Map<String, String> details);
+
+ void updateExtensionResourceMapDetails(final long extensionResourceMapId,
final Map<String, String> details);
}
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 3087f184dde..5abf0f424a7 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
@@ -1478,6 +1478,45 @@ public class ExtensionsManagerImpl extends ManagerBase
implements ExtensionsMana
return GsonHelper.getGson().toJson(answers);
}
+ @Override
+ public Pair<Boolean, ExtensionResourceMap>
extensionResourceMapDetailsNeedUpdate(long resourceId,
+ ExtensionResourceMap.ResourceType resourceType,
Map<String, String> externalDetails) {
+ if (MapUtils.isEmpty(externalDetails)) {
+ return new Pair<>(false, null);
+ }
+ ExtensionResourceMapVO extensionResourceMapVO =
+ extensionResourceMapDao.findByResourceIdAndType(resourceId,
resourceType);
+ if (extensionResourceMapVO == null) {
+ return new Pair<>(true, null);
+ }
+ Map<String, String> mapDetails =
+
extensionResourceMapDetailsDao.listDetailsKeyPairs(extensionResourceMapVO.getId());
+ if (MapUtils.isEmpty(mapDetails) || mapDetails.size() !=
externalDetails.size()) {
+ return new Pair<>(true, extensionResourceMapVO);
+ }
+ for (Map.Entry<String, String> entry : externalDetails.entrySet()) {
+ String key = entry.getKey();
+ String value = entry.getValue();
+ if (!value.equals(mapDetails.get(key))) {
+ return new Pair<>(true, extensionResourceMapVO);
+ }
+ }
+ return new Pair<>(false, extensionResourceMapVO);
+ }
+
+ @Override
+ public void updateExtensionResourceMapDetails(long extensionResourceMapId,
Map<String, String> details) {
+ if (MapUtils.isEmpty(details)) {
+ return;
+ }
+ List<ExtensionResourceMapDetailsVO> detailsList = new ArrayList<>();
+ for (Map.Entry<String, String> entry : details.entrySet()) {
+ detailsList.add(new
ExtensionResourceMapDetailsVO(extensionResourceMapId, entry.getKey(),
+ entry.getValue()));
+ }
+ extensionResourceMapDetailsDao.saveDetails(detailsList);
+ }
+
@Override
public Long getExtensionIdForCluster(long clusterId) {
ExtensionResourceMapVO map =
extensionResourceMapDao.findByResourceIdAndType(clusterId,
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 00bf915831b..fcceb16523e 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
@@ -1742,6 +1742,82 @@ public class ExtensionsManagerImplTest {
assertTrue(json.contains("\"result\":false"));
}
+ @Test
+ public void
extensionResourceMapDetailsNeedUpdateReturnsTrueWhenNoResourceMapExists() {
+ when(extensionResourceMapDao.findByResourceIdAndType(1L,
ExtensionResourceMap.ResourceType.Cluster)).thenReturn(null);
+ Map<String, String> externalDetails = Map.of("key", "value");
+ Pair<Boolean, ExtensionResourceMap> result =
extensionsManager.extensionResourceMapDetailsNeedUpdate(1L,
+ ExtensionResourceMap.ResourceType.Cluster, externalDetails);
+ assertTrue(result.first());
+ assertNull(result.second());
+ }
+
+ @Test
+ public void
extensionResourceMapDetailsNeedUpdateReturnsFalseWhenDetailsMatch() {
+ ExtensionResourceMapVO resourceMap =
mock(ExtensionResourceMapVO.class);
+ when(extensionResourceMapDao.findByResourceIdAndType(1L,
ExtensionResourceMap.ResourceType.Cluster)).thenReturn(resourceMap);
+
when(extensionResourceMapDetailsDao.listDetailsKeyPairs(resourceMap.getId())).thenReturn(Map.of("key",
"value"));
+ Map<String, String> externalDetails = Map.of("key", "value");
+ Pair<Boolean, ExtensionResourceMap> result =
extensionsManager.extensionResourceMapDetailsNeedUpdate(1L,
+ ExtensionResourceMap.ResourceType.Cluster, externalDetails);
+ assertFalse(result.first());
+ assertEquals(resourceMap, result.second());
+ }
+
+ @Test
+ public void
extensionResourceMapDetailsNeedUpdateReturnsTrueWhenDetailsDiffer() {
+ ExtensionResourceMapVO resourceMap =
mock(ExtensionResourceMapVO.class);
+ when(extensionResourceMapDao.findByResourceIdAndType(1L,
ExtensionResourceMap.ResourceType.Cluster)).thenReturn(resourceMap);
+
when(extensionResourceMapDetailsDao.listDetailsKeyPairs(resourceMap.getId())).thenReturn(Map.of("key",
"oldValue"));
+ Map<String, String> externalDetails = Map.of("key", "newValue");
+ Pair<Boolean, ExtensionResourceMap> result =
extensionsManager.extensionResourceMapDetailsNeedUpdate(1L,
+ ExtensionResourceMap.ResourceType.Cluster, externalDetails);
+ assertTrue(result.first());
+ assertEquals(resourceMap, result.second());
+ }
+
+ @Test
+ public void
extensionResourceMapDetailsNeedUpdateReturnsTrueWhenExternalDetailsHaveExtraKeys()
{
+ ExtensionResourceMapVO resourceMap =
mock(ExtensionResourceMapVO.class);
+ when(extensionResourceMapDao.findByResourceIdAndType(1L,
ExtensionResourceMap.ResourceType.Cluster)).thenReturn(resourceMap);
+
when(extensionResourceMapDetailsDao.listDetailsKeyPairs(resourceMap.getId())).thenReturn(Map.of("key",
"value"));
+ Map<String, String> externalDetails = Map.of("key", "value", "extra",
"something");
+ Pair<Boolean, ExtensionResourceMap> result =
extensionsManager.extensionResourceMapDetailsNeedUpdate(1L,
+ ExtensionResourceMap.ResourceType.Cluster, externalDetails);
+ assertTrue(result.first());
+ assertEquals(resourceMap, result.second());
+ }
+
+ @Test
+ public void
updateExtensionResourceMapDetails_SavesDetails_WhenDetailsProvided() {
+ long resourceMapId = 100L;
+ Map<String, String> details = Map.of("foo", "bar", "baz", "qux");
+ extensionsManager.updateExtensionResourceMapDetails(resourceMapId,
details);
+ verify(extensionResourceMapDetailsDao).saveDetails(any());
+ }
+
+ @Test
+ public void
updateExtensionResourceMapDetails_RemovesDetails_WhenDetailsIsNull() {
+ long resourceMapId = 101L;
+ extensionsManager.updateExtensionResourceMapDetails(resourceMapId,
null);
+ verify(extensionResourceMapDetailsDao, never()).saveDetails(any());
+ }
+
+ @Test
+ public void
updateExtensionResourceMapDetails_RemovesDetails_WhenDetailsIsEmpty() {
+ long resourceMapId = 102L;
+ extensionsManager.updateExtensionResourceMapDetails(resourceMapId,
Collections.emptyMap());
+ verify(extensionResourceMapDetailsDao, never()).saveDetails(any());
+ }
+
+ @Test(expected = CloudRuntimeException.class)
+ public void
updateExtensionResourceMapDetails_ThrowsException_WhenSaveFails() {
+ long resourceMapId = 103L;
+ Map<String, String> details = Map.of("foo", "bar");
+
doThrow(CloudRuntimeException.class).when(extensionResourceMapDetailsDao).saveDetails(any());
+ extensionsManager.updateExtensionResourceMapDetails(resourceMapId,
details);
+ }
+
@Test
public void
getExtensionIdForCluster_WhenMappingExists_ReturnsExtensionId() {
long clusterId = 1L;
diff --git a/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java
b/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java
index 999b46e9f9f..936dfd9cf95 100755
--- a/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java
+++ b/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java
@@ -189,6 +189,7 @@ import com.cloud.storage.dao.VMTemplateDao;
import com.cloud.storage.dao.VolumeDao;
import com.cloud.user.Account;
import com.cloud.user.AccountManager;
+import com.cloud.utils.Pair;
import com.cloud.utils.StringUtils;
import com.cloud.utils.Ternary;
import com.cloud.utils.UriUtils;
@@ -223,8 +224,8 @@ import com.cloud.vm.VirtualMachineManager;
import com.cloud.vm.VirtualMachineProfile;
import com.cloud.vm.VirtualMachineProfileImpl;
import com.cloud.vm.VmDetailConstants;
-import com.cloud.vm.dao.VMInstanceDetailsDao;
import com.cloud.vm.dao.VMInstanceDao;
+import com.cloud.vm.dao.VMInstanceDetailsDao;
import com.google.gson.Gson;
@Component
@@ -1224,9 +1225,18 @@ public class ResourceManagerImpl extends ManagerBase
implements ResourceManager,
String managedstate = cmd.getManagedstate();
String name = cmd.getClusterName();
CPU.CPUArch arch = cmd.getArch();
+ final Map<String, String> externalDetails = cmd.getExternalDetails();
// Verify cluster information and update the cluster if needed
boolean doUpdate = false;
+ Pair<Boolean, ExtensionResourceMap> needDetailsUpdateMapPair =
+
extensionsManager.extensionResourceMapDetailsNeedUpdate(cluster.getId(),
+ ExtensionResourceMap.ResourceType.Cluster, externalDetails);
+ if (Boolean.TRUE.equals(needDetailsUpdateMapPair.first()) &&
needDetailsUpdateMapPair.second() == null) {
+ throw new InvalidParameterValueException(
+ String.format("Cluster: %s is not registered with any
extension, details cannot be updated",
+ cluster.getName()));
+ }
if (StringUtils.isNotBlank(name)) {
if(cluster.getHypervisorType() == HypervisorType.VMware) {
@@ -1311,6 +1321,11 @@ public class ResourceManagerImpl extends ManagerBase
implements ResourceManager,
_clusterDao.update(cluster.getId(), cluster);
}
+ if (Boolean.TRUE.equals(needDetailsUpdateMapPair.first())) {
+ ExtensionResourceMap extensionResourceMap =
needDetailsUpdateMapPair.second();
+
extensionsManager.updateExtensionResourceMapDetails(extensionResourceMap.getId(),
externalDetails);
+ }
+
if (newManagedState != null &&
!newManagedState.equals(oldManagedState)) {
if (newManagedState.equals(Managed.ManagedState.Unmanaged)) {
boolean success = false;
diff --git a/ui/src/views/extension/ExternalConfigurationDetails.vue
b/ui/src/views/extension/ExternalConfigurationDetails.vue
index 4322651ea3e..e7b3f298fe2 100644
--- a/ui/src/views/extension/ExternalConfigurationDetails.vue
+++ b/ui/src/views/extension/ExternalConfigurationDetails.vue
@@ -94,7 +94,8 @@ export default {
},
methods: {
fetchData () {
- if (!['cluster'].includes(this.$route.meta.name)) {
+ if (!['cluster'].includes(this.$route.meta.name) ||
!this.resource.extensionid) {
+ this.extension = {}
return
}
this.loading = true
diff --git a/ui/src/views/infra/ClusterUpdate.vue
b/ui/src/views/infra/ClusterUpdate.vue
index a0284a6f6c8..1af7f420e66 100644
--- a/ui/src/views/infra/ClusterUpdate.vue
+++ b/ui/src/views/infra/ClusterUpdate.vue
@@ -71,6 +71,14 @@
</a-select-option>
</a-select>
</a-form-item>
+ <a-form-item name="externaldetails" ref="externaldetails"
v-if="resource.hypervisortype === 'External' && resource.extensionid">
+ <template #label>
+ <tooltip-label :title="$t('label.configuration.details')"
:tooltip="apiParams.externaldetails.description"/>
+ </template>
+ <div style="margin-bottom: 10px">{{
$t('message.add.extension.resource.details') }}</div>
+ <details-input
+ v-model:value="form.externaldetails" />
+ </a-form-item>
<div :span="24" class="action-button">
<a-button :loading="loading" @click="onCloseAction">{{
$t('label.cancel') }}</a-button>
@@ -84,11 +92,13 @@
import { ref, reactive, toRaw } from 'vue'
import { getAPI, postAPI } from '@/api'
import TooltipLabel from '@/components/widgets/TooltipLabel'
+import DetailsInput from '@/components/widgets/DetailsInput'
export default {
name: 'ClusterUpdate',
components: {
- TooltipLabel
+ TooltipLabel,
+ DetailsInput
},
props: {
action: {
@@ -145,6 +155,7 @@ export default {
fetchData () {
this.fetchArchitectureTypes()
this.fetchStorageAccessGroupsData()
+ this.fetchExtensionResourceMapDetails()
},
fetchArchitectureTypes () {
this.architectureTypes.opts = []
@@ -159,13 +170,39 @@ export default {
})
this.architectureTypes.opts = typesList
},
+ fetchExtensionResourceMapDetails () {
+ this.form.externaldetails = null
+ if (!this.resource.id || !this.resource.extensionid) {
+ return
+ }
+ this.loading = true
+ const params = {
+ id: this.resource.extensionid,
+ details: 'resource'
+ }
+ getAPI('listExtensions', params).then(json => {
+ const resources =
json?.listextensionsresponse?.extension?.[0]?.resources || []
+ const resourceMap = resources.find(r => r.id === this.resource.id)
+ if (resourceMap && resourceMap.details && typeof resourceMap.details
=== 'object') {
+ this.form.externaldetails = resourceMap.details
+ }
+ }).catch(error => {
+ this.$notifyError(error)
+ }).finally(() => {
+ this.loading = false
+ })
+ },
handleSubmit () {
this.formRef.value.validate().then(() => {
const values = toRaw(this.form)
- console.log(values)
const params = {}
params.id = this.resource.id
params.clustername = values.name
+ if (values.externaldetails) {
+ Object.entries(values.externaldetails).forEach(([key, value]) => {
+ params['externaldetails[0].' + key] = value
+ })
+ }
this.loading = true
postAPI('updateCluster', params).then(json => {