Skip to content

Commit 4aed972

Browse files
authored
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]>
1 parent 9fee6da commit 4aed972

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
@@ -189,6 +189,7 @@
189189
import com.cloud.storage.dao.VolumeDao;
190190
import com.cloud.user.Account;
191191
import com.cloud.user.AccountManager;
192+
import com.cloud.utils.Pair;
192193
import com.cloud.utils.StringUtils;
193194
import com.cloud.utils.Ternary;
194195
import com.cloud.utils.UriUtils;
@@ -223,8 +224,8 @@
223224
import com.cloud.vm.VirtualMachineProfile;
224225
import com.cloud.vm.VirtualMachineProfileImpl;
225226
import com.cloud.vm.VmDetailConstants;
226-
import com.cloud.vm.dao.VMInstanceDetailsDao;
227227
import com.cloud.vm.dao.VMInstanceDao;
228+
import com.cloud.vm.dao.VMInstanceDetailsDao;
228229
import com.google.gson.Gson;
229230

230231
@Component
@@ -1224,9 +1225,18 @@ public Cluster updateCluster(UpdateClusterCmd cmd) {
12241225
String managedstate = cmd.getManagedstate();
12251226
String name = cmd.getClusterName();
12261227
CPU.CPUArch arch = cmd.getArch();
1228+
final Map<String, String> externalDetails = cmd.getExternalDetails();
12271229

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

12311241
if (StringUtils.isNotBlank(name)) {
12321242
if(cluster.getHypervisorType() == HypervisorType.VMware) {
@@ -1311,6 +1321,11 @@ public Cluster updateCluster(UpdateClusterCmd cmd) {
13111321
_clusterDao.update(cluster.getId(), cluster);
13121322
}
13131323

1324+
if (Boolean.TRUE.equals(needDetailsUpdateMapPair.first())) {
1325+
ExtensionResourceMap extensionResourceMap = needDetailsUpdateMapPair.second();
1326+
extensionsManager.updateExtensionResourceMapDetails(extensionResourceMap.getId(), externalDetails);
1327+
}
1328+
13141329
if (newManagedState != null && !newManagedState.equals(oldManagedState)) {
13151330
if (newManagedState.equals(Managed.ManagedState.Unmanaged)) {
13161331
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)