Skip to content

Commit 249c5ae

Browse files
committed
api,server,ui: allow updating computeoffering external details
Signed-off-by: Abhishek Kumar <[email protected]>
1 parent 7e637c2 commit 249c5ae

File tree

9 files changed

+164
-28
lines changed

9 files changed

+164
-28
lines changed

api/src/main/java/org/apache/cloudstack/api/command/admin/offering/CreateServiceOfferingCmd.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ public class CreateServiceOfferingCmd extends BaseCmd {
267267
type = CommandType.MAP,
268268
description = "Details in key/value pairs using format externaldetails[i].keyname=keyvalue. Example: externaldetails[0].endpoint.url=urlvalue",
269269
since = "4.21.0")
270-
protected Map externalDetails;
270+
private Map externalDetails;
271271

272272
/////////////////////////////////////////////////////
273273
/////////////////// Accessors ///////////////////////

api/src/main/java/org/apache/cloudstack/api/command/admin/offering/UpdateServiceOfferingCmd.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.util.ArrayList;
2020
import java.util.List;
21+
import java.util.Map;
2122

2223
import com.cloud.offering.ServiceOffering.State;
2324
import org.apache.cloudstack.api.APICommand;
@@ -94,6 +95,12 @@ public class UpdateServiceOfferingCmd extends BaseCmd {
9495
since="4.20")
9596
private Boolean purgeResources;
9697

98+
@Parameter(name = ApiConstants.EXTERNAL_DETAILS,
99+
type = CommandType.MAP,
100+
description = "Details in key/value pairs using format externaldetails[i].keyname=keyvalue. Example: externaldetails[0].endpoint.url=urlvalue",
101+
since = "4.21.0")
102+
private Map externalDetails;
103+
97104
/////////////////////////////////////////////////////
98105
/////////////////// Accessors ///////////////////////
99106
/////////////////////////////////////////////////////
@@ -194,6 +201,10 @@ public boolean isPurgeResources() {
194201
return Boolean.TRUE.equals(purgeResources);
195202
}
196203

204+
public Map<String, String> getExternalDetails() {
205+
return convertExternalDetailsToMap(externalDetails);
206+
}
207+
197208
/////////////////////////////////////////////////////
198209
/////////////// API Implementation///////////////////
199210
/////////////////////////////////////////////////////

server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,7 @@
304304
import com.cloud.utils.net.NetUtils;
305305
import com.cloud.vm.NicIpAlias;
306306
import com.cloud.vm.VirtualMachine;
307+
import com.cloud.vm.VmDetailConstants;
307308
import com.cloud.vm.dao.NicIpAliasDao;
308309
import com.cloud.vm.dao.NicIpAliasVO;
309310
import com.cloud.vm.dao.VMInstanceDao;
@@ -3752,6 +3753,30 @@ private void setBytesRate(DiskOffering offering, Long bytesReadRate, Long bytesR
37523753
}
37533754
}
37543755

3756+
protected boolean serviceOfferingExternalDetailsNeedUpdate(final Map<String, String> offeringDetails,
3757+
final Map<String, String> externalDetails) {
3758+
if (MapUtils.isEmpty(externalDetails)) {
3759+
return false;
3760+
}
3761+
3762+
Map<String, String> existingExternalDetails = offeringDetails.entrySet().stream()
3763+
.filter(detail -> detail.getKey().startsWith(VmDetailConstants.EXTERNAL_DETAIL_PREFIX))
3764+
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
3765+
3766+
if (MapUtils.isEmpty(existingExternalDetails) || existingExternalDetails.size() != externalDetails.size()) {
3767+
return true;
3768+
}
3769+
3770+
for (Map.Entry<String, String> entry : externalDetails.entrySet()) {
3771+
String key = entry.getKey();
3772+
String value = entry.getValue();
3773+
if (!value.equals(existingExternalDetails.get(key))) {
3774+
return true;
3775+
}
3776+
}
3777+
return false;
3778+
}
3779+
37553780
@Override
37563781
@ActionEvent(eventType = EventTypes.EVENT_SERVICE_OFFERING_EDIT, eventDescription = "updating service offering")
37573782
public ServiceOffering updateServiceOffering(final UpdateServiceOfferingCmd cmd) {
@@ -3766,6 +3791,7 @@ public ServiceOffering updateServiceOffering(final UpdateServiceOfferingCmd cmd)
37663791
String hostTags = cmd.getHostTags();
37673792
ServiceOffering.State state = cmd.getState();
37683793
boolean purgeResources = cmd.isPurgeResources();
3794+
final Map<String, String> externalDetails = cmd.getExternalDetails();
37693795

37703796
if (userId == null) {
37713797
userId = Long.valueOf(User.UID_SYSTEM);
@@ -3783,7 +3809,8 @@ public ServiceOffering updateServiceOffering(final UpdateServiceOfferingCmd cmd)
37833809
List<Long> existingZoneIds = _serviceOfferingDetailsDao.findZoneIds(id);
37843810
Collections.sort(existingZoneIds);
37853811

3786-
String purgeResourceStr = _serviceOfferingDetailsDao.getDetail(id, ServiceOffering.PURGE_DB_ENTITIES_KEY);
3812+
Map<String, String> offeringDetails = _serviceOfferingDetailsDao.listDetailsKeyPairs(id);
3813+
String purgeResourceStr = offeringDetails.get(ServiceOffering.PURGE_DB_ENTITIES_KEY);
37873814
boolean existingPurgeResources = false;
37883815
if (StringUtils.isNotBlank(purgeResourceStr)) {
37893816
existingPurgeResources = Boolean.parseBoolean(purgeResourceStr);
@@ -3857,8 +3884,11 @@ public ServiceOffering updateServiceOffering(final UpdateServiceOfferingCmd cmd)
38573884
}
38583885

38593886
final boolean updateNeeded = name != null || displayText != null || sortKey != null || storageTags != null || hostTags != null || state != null;
3887+
final boolean serviceOfferingExternalDetailsNeedUpdate =
3888+
serviceOfferingExternalDetailsNeedUpdate(offeringDetails, externalDetails);
38603889
final boolean detailsUpdateNeeded = !filteredDomainIds.equals(existingDomainIds) ||
3861-
!filteredZoneIds.equals(existingZoneIds) || purgeResources != existingPurgeResources;
3890+
!filteredZoneIds.equals(existingZoneIds) || purgeResources != existingPurgeResources ||
3891+
serviceOfferingExternalDetailsNeedUpdate;
38623892
if (!updateNeeded && !detailsUpdateNeeded) {
38633893
return _serviceOfferingDao.findById(id);
38643894
}
@@ -3900,6 +3930,7 @@ public ServiceOffering updateServiceOffering(final UpdateServiceOfferingCmd cmd)
39003930
SearchBuilder<ServiceOfferingDetailsVO> sb = _serviceOfferingDetailsDao.createSearchBuilder();
39013931
sb.and("offeringId", sb.entity().getResourceId(), SearchCriteria.Op.EQ);
39023932
sb.and("detailName", sb.entity().getName(), SearchCriteria.Op.EQ);
3933+
sb.and("detailNameLike", sb.entity().getName(), SearchCriteria.Op.LIKE);
39033934
sb.done();
39043935
SearchCriteria<ServiceOfferingDetailsVO> sc = sb.create();
39053936
sc.setParameters("offeringId", String.valueOf(id));
@@ -3925,6 +3956,14 @@ public ServiceOffering updateServiceOffering(final UpdateServiceOfferingCmd cmd)
39253956
"true", false));
39263957
}
39273958
}
3959+
if (serviceOfferingExternalDetailsNeedUpdate) {
3960+
SearchCriteria<ServiceOfferingDetailsVO> externalDetailsRemoveSC = sb.create();
3961+
externalDetailsRemoveSC.setParameters("detailNameLike", VmDetailConstants.EXTERNAL_DETAIL_PREFIX + "%");
3962+
_serviceOfferingDetailsDao.remove(externalDetailsRemoveSC);
3963+
for (Map.Entry<String, String> entry : externalDetails.entrySet()) {
3964+
detailsVO.add(new ServiceOfferingDetailsVO(id, entry.getKey(), entry.getValue(), true));
3965+
}
3966+
}
39283967
}
39293968
if (!detailsVO.isEmpty()) {
39303969
for (ServiceOfferingDetailsVO detailVO : detailsVO) {

server/src/test/java/com/cloud/configuration/ConfigurationManagerImplTest.java

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@
8888
import java.util.ArrayList;
8989
import java.util.Collections;
9090
import java.util.List;
91+
import java.util.Map;
9192

9293
import static org.mockito.ArgumentMatchers.any;
9394
import static org.mockito.ArgumentMatchers.anyLong;
@@ -1041,4 +1042,54 @@ public void getConfigurationTypeTestReturnsStringRepresentingConfigurationType()
10411042
configurationManagerImplSpy.getConfigurationType(configuration.key());
10421043
Mockito.verify(configurationManagerImplSpy).parseConfigurationTypeIntoString(configuration.type(), configurationVOMock);
10431044
}
1045+
1046+
@Test
1047+
public void serviceOfferingExternalDetailsNeedUpdateReturnsFalseWhenExternalDetailsIsEmpty() {
1048+
Map<String, String> offeringDetails = Map.of("key1", "value1");
1049+
Map<String, String> externalDetails = Collections.emptyMap();
1050+
1051+
boolean result = configurationManagerImplSpy.serviceOfferingExternalDetailsNeedUpdate(offeringDetails, externalDetails);
1052+
1053+
Assert.assertFalse(result);
1054+
}
1055+
1056+
@Test
1057+
public void serviceOfferingExternalDetailsNeedUpdateReturnsTrueWhenExistingExternalDetailsIsEmpty() {
1058+
Map<String, String> offeringDetails = Map.of("key1", "value1");
1059+
Map<String, String> externalDetails = Map.of("External:key1", "value1");
1060+
1061+
boolean result = configurationManagerImplSpy.serviceOfferingExternalDetailsNeedUpdate(offeringDetails, externalDetails);
1062+
1063+
Assert.assertTrue(result);
1064+
}
1065+
1066+
@Test
1067+
public void serviceOfferingExternalDetailsNeedUpdateReturnsTrueWhenSizesDiffer() {
1068+
Map<String, String> offeringDetails = Map.of("External:key1", "value1");
1069+
Map<String, String> externalDetails = Map.of("External:key1", "value1", "External:key2", "value2");
1070+
1071+
boolean result = configurationManagerImplSpy.serviceOfferingExternalDetailsNeedUpdate(offeringDetails, externalDetails);
1072+
1073+
Assert.assertTrue(result);
1074+
}
1075+
1076+
@Test
1077+
public void serviceOfferingExternalDetailsNeedUpdateReturnsTrueWhenValuesDiffer() {
1078+
Map<String, String> offeringDetails = Map.of("External:key1", "value1");
1079+
Map<String, String> externalDetails = Map.of("External:key1", "differentValue");
1080+
1081+
boolean result = configurationManagerImplSpy.serviceOfferingExternalDetailsNeedUpdate(offeringDetails, externalDetails);
1082+
1083+
Assert.assertTrue(result);
1084+
}
1085+
1086+
@Test
1087+
public void serviceOfferingExternalDetailsNeedUpdateReturnsFalseWhenDetailsMatch() {
1088+
Map<String, String> offeringDetails = Map.of("External:key1", "value1", "External:key2", "value2");
1089+
Map<String, String> externalDetails = Map.of("External:key1", "value1", "External:key2", "value2");
1090+
1091+
boolean result = configurationManagerImplSpy.serviceOfferingExternalDetailsNeedUpdate(offeringDetails, externalDetails);
1092+
1093+
Assert.assertFalse(result);
1094+
}
10441095
}

ui/src/config/section/offering.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
// under the License.
1717
import { shallowRef, defineAsyncComponent } from 'vue'
1818
import store from '@/store'
19+
import { getFilteredExternalDetails } from '@/utils/extension'
1920

2021
export default {
2122
name: 'offering',
@@ -95,7 +96,12 @@ export default {
9596
label: 'label.edit',
9697
docHelp: 'adminguide/service_offerings.html#modifying-or-deleting-a-service-offering',
9798
dataView: true,
98-
args: ['name', 'displaytext', 'storageaccessgroups', 'hosttags']
99+
args: ['name', 'displaytext', 'storageaccessgroups', 'hosttags', 'externaldetails'],
100+
mapping: {
101+
externaldetails: {
102+
transformedvalue: (record) => { return getFilteredExternalDetails(record.serviceofferingdetails) }
103+
}
104+
}
99105
}, {
100106
api: 'updateServiceOffering',
101107
icon: 'lock-outlined',

ui/src/utils/extension.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
export function getFilteredExternalDetails (details) {
19+
if (!details || typeof details !== 'object') {
20+
return null
21+
}
22+
const prefix = 'External:'
23+
const result = {}
24+
for (const key in details) {
25+
if (key.startsWith(prefix)) {
26+
result[key.substring(prefix.length)] = details[key]
27+
}
28+
}
29+
return Object.keys(result).length > 0 ? result : null
30+
}

ui/src/views/AutogenView.vue

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,9 @@
363363
{{ opt.name && opt.type ? opt.name + ' (' + opt.type + ')' : opt.name || opt.description }}
364364
</a-select-option>
365365
</a-select>
366+
<details-input
367+
v-else-if="field.type==='map'"
368+
v-model:value="form[field.name]" />
366369
<a-input-number
367370
v-else-if="field.type==='long'"
368371
v-focus="fieldIndex === firstIndex"
@@ -473,6 +476,7 @@ import OsLogo from '@/components/widgets/OsLogo'
473476
import ResourceIcon from '@/components/view/ResourceIcon'
474477
import BulkActionProgress from '@/components/view/BulkActionProgress'
475478
import TooltipLabel from '@/components/widgets/TooltipLabel'
479+
import DetailsInput from '@/components/widgets/DetailsInput'
476480
477481
export default {
478482
name: 'Resource',
@@ -485,7 +489,8 @@ export default {
485489
BulkActionProgress,
486490
TooltipLabel,
487491
OsLogo,
488-
ResourceIcon
492+
ResourceIcon,
493+
DetailsInput
489494
},
490495
mixins: [mixinDevice],
491496
provide: function () {
@@ -1423,6 +1428,15 @@ export default {
14231428
this.form[field.name] = fieldValue
14241429
} else if (field.type === 'boolean' && field.name === 'rebalance' && this.currentAction.api === 'cancelMaintenance') {
14251430
this.form[field.name] = true
1431+
} else if (field.type === 'map') {
1432+
const transformedValue = this.currentAction.mapping?.[field.name]?.transformedvalue
1433+
if (typeof transformedValue === 'function') {
1434+
this.form[field.name] = transformedValue(this.resource)
1435+
} else if (typeof transformedValue === 'object' && transformedValue !== null) {
1436+
this.form[field.name] = { ...transformedValue }
1437+
} else {
1438+
this.form[field.name] = {}
1439+
}
14261440
}
14271441
})
14281442
},
@@ -1623,6 +1637,10 @@ export default {
16231637
} else {
16241638
params[key] = param.opts[input].name
16251639
}
1640+
} else if (param.type === 'map' && typeof input === 'object') {
1641+
Object.entries(values.externaldetails).forEach(([key, value]) => {
1642+
params[param.name + '[0].' + key] = value
1643+
})
16261644
} else {
16271645
params[key] = input
16281646
}

ui/src/views/extension/ExternalConfigurationDetails.vue

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
<script>
3838
import { getAPI } from '@/api'
3939
import ObjectListTable from '@/components/view/ObjectListTable'
40+
import { getFilteredExternalDetails } from '@/utils/extension'
4041
4142
export default {
4243
name: 'ExternalConfigurationDetails',
@@ -78,18 +79,7 @@ export default {
7879
if (!detailsKey || !this.resource) {
7980
return null
8081
}
81-
const details = this.resource[detailsKey]
82-
if (!details || typeof details !== 'object') {
83-
return null
84-
}
85-
const prefix = 'External:'
86-
const result = {}
87-
for (const key in details) {
88-
if (key.startsWith(prefix)) {
89-
result[key.substring(prefix.length)] = details[key]
90-
}
91-
}
92-
return Object.keys(result).length > 0 ? result : null
82+
return getFilteredExternalDetails(this.resource[detailsKey])
9383
},
9484
extensionResourceDetails () {
9585
if (!this.resource?.id || !this.extension?.resources) {

ui/src/views/infra/HostUpdate.vue

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ import { ref, reactive, toRaw } from 'vue'
103103
import { getAPI, postAPI } from '@/api'
104104
import TooltipLabel from '@/components/widgets/TooltipLabel'
105105
import DetailsInput from '@/components/widgets/DetailsInput'
106+
import { getFilteredExternalDetails } from '@/utils/extension'
106107
107108
export default {
108109
name: 'HostUpdate',
@@ -142,17 +143,7 @@ export default {
142143
},
143144
computed: {
144145
resourceExternalDetails () {
145-
const prefix = 'External:'
146-
if (!this.resource.details || typeof this.resource.details !== 'object' || Object.keys(this.resource.details).length === 0) {
147-
return null
148-
}
149-
const result = {}
150-
for (const key in this.resource.details) {
151-
if (key.startsWith(prefix)) {
152-
result[key.substring(prefix.length)] = this.resource.details[key]
153-
}
154-
}
155-
return result
146+
return getFilteredExternalDetails(this.resource.details)
156147
}
157148
},
158149
methods: {

0 commit comments

Comments
 (0)