mirror of
https://github.com/apache/cloudstack.git
synced 2025-10-26 08:42:29 +01:00
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.mrt22@gmail.com>
This commit is contained in:
parent
9fee6dae34
commit
4aed972e78
@ -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());
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user