Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -122,6 +130,10 @@ public CPU.CPUArch getArch() {
return CPU.CPUArch.fromType(arch);
}

public Map<String, String> getExternalDetails() {
return convertDetailsToMap(externalDetails);
}

@Override
public void execute() {
Cluster cluster = _resourceService.getCluster(getId());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@

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 {
Expand Down Expand Up @@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -1478,6 +1478,45 @@ public String handleExtensionServerCommands(ExtensionServerActionBaseCommand com
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1742,6 +1742,82 @@ public void handleExtensionServerCommands_UnsupportedCommand_ReturnsUnsupportedA
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;
Expand Down
17 changes: 16 additions & 1 deletion server/src/main/java/com/cloud/resource/ResourceManagerImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@
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;
Expand Down Expand Up @@ -223,8 +224,8 @@
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
Expand Down Expand Up @@ -1224,9 +1225,18 @@ public Cluster updateCluster(UpdateClusterCmd cmd) {
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) {
Expand Down Expand Up @@ -1311,6 +1321,11 @@ public Cluster updateCluster(UpdateClusterCmd cmd) {
_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;
Expand Down
3 changes: 2 additions & 1 deletion ui/src/views/extension/ExternalConfigurationDetails.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
44 changes: 43 additions & 1 deletion ui/src/views/infra/ClusterUpdate.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -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: {
Expand Down Expand Up @@ -145,6 +155,7 @@ export default {
fetchData () {
this.fetchArchitectureTypes()
this.fetchStorageAccessGroupsData()
this.fetchExtensionResourceMapDetails()
},
fetchArchitectureTypes () {
this.architectureTypes.opts = []
Expand All @@ -159,13 +170,44 @@ 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 => {
this.extension = json.listextensionsresponse.extension[0]
if (!this.extension?.resources) {
return null
}
const resourceMap = this.extension.resources.find(r => r.id === this.resource.id)
if (!resourceMap || !resourceMap.details || typeof resourceMap.details !== 'object') {
this.form.externaldetails = null
}
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 => {
Expand Down
Loading