Skip to content

Commit fc3c42b

Browse files
shwstpprdhslove
authored andcommitted
api,server,extensions: allow updating extension resource map details (apache#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]>
1 parent 9978c04 commit fc3c42b

File tree

7 files changed

+190
-4
lines changed

7 files changed

+190
-4
lines changed

api/src/main/java/org/apache/cloudstack/api/command/admin/cluster/UpdateClusterCmd.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
// under the License.
1717
package org.apache.cloudstack.api.command.admin.cluster;
1818

19+
import java.util.Map;
20+
1921
import com.cloud.cpu.CPU;
2022
import org.apache.cloudstack.api.ApiCommandResourceType;
2123

@@ -60,6 +62,12 @@ public class UpdateClusterCmd extends BaseCmd {
6062
since = "4.20")
6163
private String arch;
6264

65+
@Parameter(name = ApiConstants.EXTERNAL_DETAILS,
66+
type = CommandType.MAP,
67+
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",
68+
since = "4.21.0")
69+
protected Map externalDetails;
70+
6371
public String getClusterName() {
6472
return clusterName;
6573
}
@@ -122,6 +130,10 @@ public CPU.CPUArch getArch() {
122130
return CPU.CPUArch.fromType(arch);
123131
}
124132

133+
public Map<String, String> getExternalDetails() {
134+
return convertDetailsToMap(externalDetails);
135+
}
136+
125137
@Override
126138
public void execute() {
127139
Cluster cluster = _resourceService.getCluster(getId());

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646

4747
import com.cloud.host.Host;
4848
import com.cloud.org.Cluster;
49+
import com.cloud.utils.Pair;
4950
import com.cloud.utils.component.Manager;
5051

5152
public interface ExtensionsManager extends Manager {
@@ -87,4 +88,9 @@ public interface ExtensionsManager extends Manager {
8788
Map<String, Map<String, String>> getExternalAccessDetails(Host host, Map<String, String> vmDetails);
8889

8990
String handleExtensionServerCommands(ExtensionServerActionBaseCommand cmd);
91+
92+
Pair<Boolean, ExtensionResourceMap> extensionResourceMapDetailsNeedUpdate(final long resourceId,
93+
final ExtensionResourceMap.ResourceType resourceType, final Map<String, String> details);
94+
95+
void updateExtensionResourceMapDetails(final long extensionResourceMapId, final Map<String, String> details);
9096
}

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1478,6 +1478,45 @@ public String handleExtensionServerCommands(ExtensionServerActionBaseCommand com
14781478
return GsonHelper.getGson().toJson(answers);
14791479
}
14801480

1481+
@Override
1482+
public Pair<Boolean, ExtensionResourceMap> extensionResourceMapDetailsNeedUpdate(long resourceId,
1483+
ExtensionResourceMap.ResourceType resourceType, Map<String, String> externalDetails) {
1484+
if (MapUtils.isEmpty(externalDetails)) {
1485+
return new Pair<>(false, null);
1486+
}
1487+
ExtensionResourceMapVO extensionResourceMapVO =
1488+
extensionResourceMapDao.findByResourceIdAndType(resourceId, resourceType);
1489+
if (extensionResourceMapVO == null) {
1490+
return new Pair<>(true, null);
1491+
}
1492+
Map<String, String> mapDetails =
1493+
extensionResourceMapDetailsDao.listDetailsKeyPairs(extensionResourceMapVO.getId());
1494+
if (MapUtils.isEmpty(mapDetails) || mapDetails.size() != externalDetails.size()) {
1495+
return new Pair<>(true, extensionResourceMapVO);
1496+
}
1497+
for (Map.Entry<String, String> entry : externalDetails.entrySet()) {
1498+
String key = entry.getKey();
1499+
String value = entry.getValue();
1500+
if (!value.equals(mapDetails.get(key))) {
1501+
return new Pair<>(true, extensionResourceMapVO);
1502+
}
1503+
}
1504+
return new Pair<>(false, extensionResourceMapVO);
1505+
}
1506+
1507+
@Override
1508+
public void updateExtensionResourceMapDetails(long extensionResourceMapId, Map<String, String> details) {
1509+
if (MapUtils.isEmpty(details)) {
1510+
return;
1511+
}
1512+
List<ExtensionResourceMapDetailsVO> detailsList = new ArrayList<>();
1513+
for (Map.Entry<String, String> entry : details.entrySet()) {
1514+
detailsList.add(new ExtensionResourceMapDetailsVO(extensionResourceMapId, entry.getKey(),
1515+
entry.getValue()));
1516+
}
1517+
extensionResourceMapDetailsDao.saveDetails(detailsList);
1518+
}
1519+
14811520
@Override
14821521
public Long getExtensionIdForCluster(long clusterId) {
14831522
ExtensionResourceMapVO map = extensionResourceMapDao.findByResourceIdAndType(clusterId,

framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManagerImplTest.java

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1742,6 +1742,82 @@ public void handleExtensionServerCommands_UnsupportedCommand_ReturnsUnsupportedA
17421742
assertTrue(json.contains("\"result\":false"));
17431743
}
17441744

1745+
@Test
1746+
public void extensionResourceMapDetailsNeedUpdateReturnsTrueWhenNoResourceMapExists() {
1747+
when(extensionResourceMapDao.findByResourceIdAndType(1L, ExtensionResourceMap.ResourceType.Cluster)).thenReturn(null);
1748+
Map<String, String> externalDetails = Map.of("key", "value");
1749+
Pair<Boolean, ExtensionResourceMap> result = extensionsManager.extensionResourceMapDetailsNeedUpdate(1L,
1750+
ExtensionResourceMap.ResourceType.Cluster, externalDetails);
1751+
assertTrue(result.first());
1752+
assertNull(result.second());
1753+
}
1754+
1755+
@Test
1756+
public void extensionResourceMapDetailsNeedUpdateReturnsFalseWhenDetailsMatch() {
1757+
ExtensionResourceMapVO resourceMap = mock(ExtensionResourceMapVO.class);
1758+
when(extensionResourceMapDao.findByResourceIdAndType(1L, ExtensionResourceMap.ResourceType.Cluster)).thenReturn(resourceMap);
1759+
when(extensionResourceMapDetailsDao.listDetailsKeyPairs(resourceMap.getId())).thenReturn(Map.of("key", "value"));
1760+
Map<String, String> externalDetails = Map.of("key", "value");
1761+
Pair<Boolean, ExtensionResourceMap> result = extensionsManager.extensionResourceMapDetailsNeedUpdate(1L,
1762+
ExtensionResourceMap.ResourceType.Cluster, externalDetails);
1763+
assertFalse(result.first());
1764+
assertEquals(resourceMap, result.second());
1765+
}
1766+
1767+
@Test
1768+
public void extensionResourceMapDetailsNeedUpdateReturnsTrueWhenDetailsDiffer() {
1769+
ExtensionResourceMapVO resourceMap = mock(ExtensionResourceMapVO.class);
1770+
when(extensionResourceMapDao.findByResourceIdAndType(1L, ExtensionResourceMap.ResourceType.Cluster)).thenReturn(resourceMap);
1771+
when(extensionResourceMapDetailsDao.listDetailsKeyPairs(resourceMap.getId())).thenReturn(Map.of("key", "oldValue"));
1772+
Map<String, String> externalDetails = Map.of("key", "newValue");
1773+
Pair<Boolean, ExtensionResourceMap> result = extensionsManager.extensionResourceMapDetailsNeedUpdate(1L,
1774+
ExtensionResourceMap.ResourceType.Cluster, externalDetails);
1775+
assertTrue(result.first());
1776+
assertEquals(resourceMap, result.second());
1777+
}
1778+
1779+
@Test
1780+
public void extensionResourceMapDetailsNeedUpdateReturnsTrueWhenExternalDetailsHaveExtraKeys() {
1781+
ExtensionResourceMapVO resourceMap = mock(ExtensionResourceMapVO.class);
1782+
when(extensionResourceMapDao.findByResourceIdAndType(1L, ExtensionResourceMap.ResourceType.Cluster)).thenReturn(resourceMap);
1783+
when(extensionResourceMapDetailsDao.listDetailsKeyPairs(resourceMap.getId())).thenReturn(Map.of("key", "value"));
1784+
Map<String, String> externalDetails = Map.of("key", "value", "extra", "something");
1785+
Pair<Boolean, ExtensionResourceMap> result = extensionsManager.extensionResourceMapDetailsNeedUpdate(1L,
1786+
ExtensionResourceMap.ResourceType.Cluster, externalDetails);
1787+
assertTrue(result.first());
1788+
assertEquals(resourceMap, result.second());
1789+
}
1790+
1791+
@Test
1792+
public void updateExtensionResourceMapDetails_SavesDetails_WhenDetailsProvided() {
1793+
long resourceMapId = 100L;
1794+
Map<String, String> details = Map.of("foo", "bar", "baz", "qux");
1795+
extensionsManager.updateExtensionResourceMapDetails(resourceMapId, details);
1796+
verify(extensionResourceMapDetailsDao).saveDetails(any());
1797+
}
1798+
1799+
@Test
1800+
public void updateExtensionResourceMapDetails_RemovesDetails_WhenDetailsIsNull() {
1801+
long resourceMapId = 101L;
1802+
extensionsManager.updateExtensionResourceMapDetails(resourceMapId, null);
1803+
verify(extensionResourceMapDetailsDao, never()).saveDetails(any());
1804+
}
1805+
1806+
@Test
1807+
public void updateExtensionResourceMapDetails_RemovesDetails_WhenDetailsIsEmpty() {
1808+
long resourceMapId = 102L;
1809+
extensionsManager.updateExtensionResourceMapDetails(resourceMapId, Collections.emptyMap());
1810+
verify(extensionResourceMapDetailsDao, never()).saveDetails(any());
1811+
}
1812+
1813+
@Test(expected = CloudRuntimeException.class)
1814+
public void updateExtensionResourceMapDetails_ThrowsException_WhenSaveFails() {
1815+
long resourceMapId = 103L;
1816+
Map<String, String> details = Map.of("foo", "bar");
1817+
doThrow(CloudRuntimeException.class).when(extensionResourceMapDetailsDao).saveDetails(any());
1818+
extensionsManager.updateExtensionResourceMapDetails(resourceMapId, details);
1819+
}
1820+
17451821
@Test
17461822
public void getExtensionIdForCluster_WhenMappingExists_ReturnsExtensionId() {
17471823
long clusterId = 1L;

server/src/main/java/com/cloud/resource/ResourceManagerImpl.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@
191191
import com.cloud.storage.dao.VolumeDao;
192192
import com.cloud.user.Account;
193193
import com.cloud.user.AccountManager;
194+
import com.cloud.utils.Pair;
194195
import com.cloud.utils.StringUtils;
195196
import com.cloud.utils.Ternary;
196197
import com.cloud.utils.UriUtils;
@@ -225,8 +226,8 @@
225226
import com.cloud.vm.VirtualMachineProfile;
226227
import com.cloud.vm.VirtualMachineProfileImpl;
227228
import com.cloud.vm.VmDetailConstants;
228-
import com.cloud.vm.dao.VMInstanceDetailsDao;
229229
import com.cloud.vm.dao.VMInstanceDao;
230+
import com.cloud.vm.dao.VMInstanceDetailsDao;
230231
import com.google.gson.Gson;
231232

232233
@Component
@@ -1226,9 +1227,18 @@ public Cluster updateCluster(UpdateClusterCmd cmd) {
12261227
String managedstate = cmd.getManagedstate();
12271228
String name = cmd.getClusterName();
12281229
CPU.CPUArch arch = cmd.getArch();
1230+
final Map<String, String> externalDetails = cmd.getExternalDetails();
12291231

12301232
// Verify cluster information and update the cluster if needed
12311233
boolean doUpdate = false;
1234+
Pair<Boolean, ExtensionResourceMap> needDetailsUpdateMapPair =
1235+
extensionsManager.extensionResourceMapDetailsNeedUpdate(cluster.getId(),
1236+
ExtensionResourceMap.ResourceType.Cluster, externalDetails);
1237+
if (Boolean.TRUE.equals(needDetailsUpdateMapPair.first()) && needDetailsUpdateMapPair.second() == null) {
1238+
throw new InvalidParameterValueException(
1239+
String.format("Cluster: %s is not registered with any extension, details cannot be updated",
1240+
cluster.getName()));
1241+
}
12321242

12331243
if (StringUtils.isNotBlank(name)) {
12341244
if(cluster.getHypervisorType() == HypervisorType.VMware) {
@@ -1313,6 +1323,11 @@ public Cluster updateCluster(UpdateClusterCmd cmd) {
13131323
_clusterDao.update(cluster.getId(), cluster);
13141324
}
13151325

1326+
if (Boolean.TRUE.equals(needDetailsUpdateMapPair.first())) {
1327+
ExtensionResourceMap extensionResourceMap = needDetailsUpdateMapPair.second();
1328+
extensionsManager.updateExtensionResourceMapDetails(extensionResourceMap.getId(), externalDetails);
1329+
}
1330+
13161331
if (newManagedState != null && !newManagedState.equals(oldManagedState)) {
13171332
if (newManagedState.equals(Managed.ManagedState.Unmanaged)) {
13181333
boolean success = false;

ui/src/views/extension/ExternalConfigurationDetails.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,8 @@ export default {
9494
},
9595
methods: {
9696
fetchData () {
97-
if (!['cluster'].includes(this.$route.meta.name)) {
97+
if (!['cluster'].includes(this.$route.meta.name) || !this.resource.extensionid) {
98+
this.extension = {}
9899
return
99100
}
100101
this.loading = true

ui/src/views/infra/ClusterUpdate.vue

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,14 @@
7171
</a-select-option>
7272
</a-select>
7373
</a-form-item>
74+
<a-form-item name="externaldetails" ref="externaldetails" v-if="resource.hypervisortype === 'External' && resource.extensionid">
75+
<template #label>
76+
<tooltip-label :title="$t('label.configuration.details')" :tooltip="apiParams.externaldetails.description"/>
77+
</template>
78+
<div style="margin-bottom: 10px">{{ $t('message.add.extension.resource.details') }}</div>
79+
<details-input
80+
v-model:value="form.externaldetails" />
81+
</a-form-item>
7482

7583
<div :span="24" class="action-button">
7684
<a-button :loading="loading" @click="onCloseAction">{{ $t('label.cancel') }}</a-button>
@@ -84,11 +92,13 @@
8492
import { ref, reactive, toRaw } from 'vue'
8593
import { getAPI, postAPI } from '@/api'
8694
import TooltipLabel from '@/components/widgets/TooltipLabel'
95+
import DetailsInput from '@/components/widgets/DetailsInput'
8796
8897
export default {
8998
name: 'ClusterUpdate',
9099
components: {
91-
TooltipLabel
100+
TooltipLabel,
101+
DetailsInput
92102
},
93103
props: {
94104
action: {
@@ -145,6 +155,7 @@ export default {
145155
fetchData () {
146156
this.fetchArchitectureTypes()
147157
this.fetchStorageAccessGroupsData()
158+
this.fetchExtensionResourceMapDetails()
148159
},
149160
fetchArchitectureTypes () {
150161
this.architectureTypes.opts = []
@@ -159,13 +170,39 @@ export default {
159170
})
160171
this.architectureTypes.opts = typesList
161172
},
173+
fetchExtensionResourceMapDetails () {
174+
this.form.externaldetails = null
175+
if (!this.resource.id || !this.resource.extensionid) {
176+
return
177+
}
178+
this.loading = true
179+
const params = {
180+
id: this.resource.extensionid,
181+
details: 'resource'
182+
}
183+
getAPI('listExtensions', params).then(json => {
184+
const resources = json?.listextensionsresponse?.extension?.[0]?.resources || []
185+
const resourceMap = resources.find(r => r.id === this.resource.id)
186+
if (resourceMap && resourceMap.details && typeof resourceMap.details === 'object') {
187+
this.form.externaldetails = resourceMap.details
188+
}
189+
}).catch(error => {
190+
this.$notifyError(error)
191+
}).finally(() => {
192+
this.loading = false
193+
})
194+
},
162195
handleSubmit () {
163196
this.formRef.value.validate().then(() => {
164197
const values = toRaw(this.form)
165-
console.log(values)
166198
const params = {}
167199
params.id = this.resource.id
168200
params.clustername = values.name
201+
if (values.externaldetails) {
202+
Object.entries(values.externaldetails).forEach(([key, value]) => {
203+
params['externaldetails[0].' + key] = value
204+
})
205+
}
169206
this.loading = true
170207
171208
postAPI('updateCluster', params).then(json => {

0 commit comments

Comments
 (0)