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 <abhishek.mr...@gmail.com>
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 <abhishek.mr...@gmail.com>
---
 .../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 => {

Reply via email to