diff --git a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java index 91b8bbe1834..d2511fc41ef 100644 --- a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java +++ b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java @@ -3929,11 +3929,11 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati return sourceDiskOfferingId != null ? _diskOfferingDao.findById(sourceDiskOfferingId) : null; } - private T getOrDefault(T cmdValue, T defaultValue) { + public T getOrDefault(T cmdValue, T defaultValue) { return cmdValue != null ? cmdValue : defaultValue; } - private Boolean resolveBooleanParam(Map requestParams, String paramKey, + public Boolean resolveBooleanParam(Map requestParams, String paramKey, java.util.function.Supplier cmdValueSupplier, Boolean defaultValue) { return requestParams != null && requestParams.containsKey(paramKey) ? cmdValueSupplier.get() : defaultValue; } @@ -8287,7 +8287,39 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati logger.info("Cloning network offering {} (id: {}) to new offering with name: {}", sourceOffering.getName(), sourceOfferingId, name); - // Resolve parameters from source offering and apply add/drop logic + String detectedProvider = cmd.getProvider(); + + if (detectedProvider == null || detectedProvider.isEmpty()) { + Map> sourceServiceProviderMap = + _networkModel.getNetworkOfferingServiceProvidersMap(sourceOfferingId); + + if (sourceServiceProviderMap.containsKey(Network.Service.NetworkACL)) { + Set networkAclProviders = sourceServiceProviderMap.get(Network.Service.NetworkACL); + if (networkAclProviders != null && !networkAclProviders.isEmpty()) { + Network.Provider provider = networkAclProviders.iterator().next(); + if (provider == Network.Provider.Nsx) { + detectedProvider = "NSX"; + } else if (provider == Network.Provider.Netris) { + detectedProvider = "Netris"; + } + } + } + } + + // If this is an NSX/Netris offering, prevent network mode changes + if (detectedProvider != null && (detectedProvider.equals("NSX") || detectedProvider.equals("Netris"))) { + String cmdNetworkMode = cmd.getNetworkMode(); + if (cmdNetworkMode != null && sourceOffering.getNetworkMode() != null) { + if (!cmdNetworkMode.equalsIgnoreCase(sourceOffering.getNetworkMode().toString())) { + throw new InvalidParameterValueException( + String.format("Cannot change network mode when cloning %s provider network offerings. " + + "Source offering has network mode '%s', but '%s' was specified. " + + "The network mode is determined by the provider configuration and cannot be modified.", + detectedProvider, sourceOffering.getNetworkMode(), cmdNetworkMode)); + } + } + } + applySourceOfferingValuesToCloneCmd(cmd, sourceOffering); return createNetworkOffering(cmd); @@ -8443,7 +8475,20 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati .anyMatch(key -> key.startsWith(ApiConstants.SERVICE_CAPABILITY_LIST)); if (!hasCapabilityParams && sourceServiceCapabilityList != null && !sourceServiceCapabilityList.isEmpty()) { - setField(cmd, "serviceCapabilitiesList", sourceServiceCapabilityList); + // Filter capabilities to only include those for services in the final service list + // This ensures that if services are dropped, their capabilities are also removed + Map> filteredCapabilities = new HashMap<>(); + for (Map.Entry> entry : sourceServiceCapabilityList.entrySet()) { + Map capabilityMap = entry.getValue(); + String serviceName = capabilityMap.get("service"); + if (serviceName != null && finalServices.contains(serviceName)) { + filteredCapabilities.put(entry.getKey(), capabilityMap); + } + } + + if (!filteredCapabilities.isEmpty()) { + setField(cmd, "serviceCapabilitiesList", filteredCapabilities); + } } applyIfNotProvided(cmd, requestParams, "displayText", ApiConstants.DISPLAY_TEXT, cmd.getDisplayText(), sourceOffering.getDisplayText()); diff --git a/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java b/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java index 4595ebd4c6b..3cf03d8bfae 100644 --- a/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java +++ b/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java @@ -1024,7 +1024,10 @@ public class VpcManagerImpl extends ManagerBase implements VpcManager, VpcProvis if ((cmd.getServiceCapabilityList() == null || cmd.getServiceCapabilityList().isEmpty()) && sourceServiceCapabilityList != null && !sourceServiceCapabilityList.isEmpty()) { - ConfigurationManagerImpl.setField(cmd, "serviceCapabilityList", sourceServiceCapabilityList); + Map filteredCapabilities = filterServiceCapabilities(sourceServiceCapabilityList, finalServices); + if (!filteredCapabilities.isEmpty()) { + ConfigurationManagerImpl.setField(cmd, "serviceCapabilityList", filteredCapabilities); + } } if (cmd.getDisplayText() == null && sourceOffering.getDisplayText() != null) { @@ -1095,6 +1098,47 @@ public class VpcManagerImpl extends ManagerBase implements VpcManager, VpcProvis return null; } + /** + * Filters service capabilities to only include those for services present in the final services list. + * This ensures that when services are dropped during cloning, their associated capabilities are also removed. + * + * @param sourceServiceCapabilityList The original capability list from the source VPC offering + * in format: Map with keys like "0.service", "0.capabilitytype", "0.capabilityvalue" + * @param finalServices The list of service names that should be retained in the cloned offering + * @return Filtered map containing only capabilities for services in finalServices + */ + private Map filterServiceCapabilities(Map sourceServiceCapabilityList, + List finalServices) { + Map filteredCapabilities = new HashMap<>(); + + for (Map.Entry entry : sourceServiceCapabilityList.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + + // Check if this is a service key (e.g., "0.service", "1.service") + if (key.endsWith(".service")) { + String serviceName = value; + if (finalServices.contains(serviceName)) { + // Include this service and its associated capability entries + String prefix = key.substring(0, key.lastIndexOf('.')); + filteredCapabilities.put(key, value); + + // Also include the capability type and value for this service + String capabilityTypeKey = prefix + ".capabilitytype"; + String capabilityValueKey = prefix + ".capabilityvalue"; + if (sourceServiceCapabilityList.containsKey(capabilityTypeKey)) { + filteredCapabilities.put(capabilityTypeKey, sourceServiceCapabilityList.get(capabilityTypeKey)); + } + if (sourceServiceCapabilityList.containsKey(capabilityValueKey)) { + filteredCapabilities.put(capabilityValueKey, sourceServiceCapabilityList.get(capabilityValueKey)); + } + } + } + } + + return filteredCapabilities; + } + private void validateConnectivtyServiceCapabilities(final Set providers, final Map serviceCapabilitystList) { if (serviceCapabilitystList != null && !serviceCapabilitystList.isEmpty()) { final Collection serviceCapabilityCollection = serviceCapabilitystList.values(); diff --git a/server/src/test/java/com/cloud/configuration/ConfigurationManagerCloneIntegrationTest.java b/server/src/test/java/com/cloud/configuration/ConfigurationManagerCloneIntegrationTest.java index dcd296977dd..6f0529e40a3 100644 --- a/server/src/test/java/com/cloud/configuration/ConfigurationManagerCloneIntegrationTest.java +++ b/server/src/test/java/com/cloud/configuration/ConfigurationManagerCloneIntegrationTest.java @@ -22,7 +22,6 @@ import com.cloud.domain.DomainVO; import com.cloud.domain.dao.DomainDao; import com.cloud.exception.InvalidParameterValueException; import com.cloud.network.Network; -import com.cloud.network.NetworkModel; import com.cloud.network.Networks; import com.cloud.offering.DiskOffering; import com.cloud.offering.NetworkOffering; @@ -680,10 +679,9 @@ public class ConfigurationManagerCloneIntegrationTest { when(sourceOffering.getAvailability()).thenReturn(NetworkOffering.Availability.Optional); when(sourceOffering.getState()).thenReturn(NetworkOffering.State.Enabled); when(sourceOffering.isDefault()).thenReturn(false); - when(sourceOffering.getConserveMode()).thenReturn(true); + when(sourceOffering.isConserveMode()).thenReturn(true); when(sourceOffering.isEgressDefaultPolicy()).thenReturn(false); when(sourceOffering.isPersistent()).thenReturn(false); - when(sourceOffering.getInternetProtocol()).thenReturn("ipv4"); CloneNetworkOfferingCmd cmd = mock(CloneNetworkOfferingCmd.class); when(cmd.getSourceOfferingId()).thenReturn(sourceId); diff --git a/server/src/test/java/com/cloud/configuration/ConfigurationManagerImplTest.java b/server/src/test/java/com/cloud/configuration/ConfigurationManagerImplTest.java index 1a7df738fff..eff716f5925 100644 --- a/server/src/test/java/com/cloud/configuration/ConfigurationManagerImplTest.java +++ b/server/src/test/java/com/cloud/configuration/ConfigurationManagerImplTest.java @@ -40,11 +40,9 @@ import com.cloud.network.dao.PhysicalNetworkDao; import com.cloud.network.element.NsxProviderVO; import com.cloud.offering.DiskOffering; import com.cloud.offering.NetworkOffering; -import com.cloud.offering.ServiceOffering; import com.cloud.offerings.NetworkOfferingVO; import com.cloud.offerings.dao.NetworkOfferingDao; import com.cloud.service.ServiceOfferingVO; -import com.cloud.service.dao.ServiceOfferingDao; import com.cloud.storage.DiskOfferingVO; import com.cloud.storage.Storage; import com.cloud.storage.StorageManager; @@ -1181,7 +1179,7 @@ public class ConfigurationManagerImplTest { Long sourceOfferingId = 1L; ServiceOfferingVO sourceOffering = Mockito.mock(ServiceOfferingVO.class); DiskOfferingVO sourceDiskOffering = Mockito.mock(DiskOfferingVO.class); - + when(sourceOffering.getId()).thenReturn(sourceOfferingId); when(sourceOffering.getDisplayText()).thenReturn("Source Display Text"); when(sourceOffering.getCpu()).thenReturn(2); @@ -1213,7 +1211,7 @@ public class ConfigurationManagerImplTest { @Test public void testCloneServiceOfferingValidatesSourceOfferingExists() { Long sourceOfferingId = 999L; - + try (MockedStatic callContextMock = Mockito.mockStatic(CallContext.class)) { CallContext callContext = Mockito.mock(CallContext.class); callContextMock.when(CallContext::current).thenReturn(callContext); @@ -1227,7 +1225,7 @@ public class ConfigurationManagerImplTest { public void testCloneDiskOfferingWithAllParameters() { Long sourceOfferingId = 1L; DiskOfferingVO sourceOffering = Mockito.mock(DiskOfferingVO.class); - + when(sourceOffering.getId()).thenReturn(sourceOfferingId); when(sourceOffering.getDisplayText()).thenReturn("Source Disk Display Text"); when(sourceOffering.getProvisioningType()).thenReturn(Storage.ProvisioningType.THIN); @@ -1252,7 +1250,7 @@ public class ConfigurationManagerImplTest { @Test public void testCloneDiskOfferingValidatesSourceOfferingExists() { Long sourceOfferingId = 999L; - + try (MockedStatic callContextMock = Mockito.mockStatic(CallContext.class)) { CallContext callContext = Mockito.mock(CallContext.class); callContextMock.when(CallContext::current).thenReturn(callContext); @@ -1262,18 +1260,11 @@ public class ConfigurationManagerImplTest { } } - @Test - public void testCloneNetworkOfferingValidatesSourceOfferingExists() { - Long sourceOfferingId = 999L; - - Assert.assertNotNull(sourceOfferingId); - } - @Test public void testCloneNetworkOfferingRequiresName() { Long sourceOfferingId = 1L; NetworkOfferingVO sourceOffering = Mockito.mock(NetworkOfferingVO.class); - + when(sourceOffering.getId()).thenReturn(sourceOfferingId); when(sourceOffering.getName()).thenReturn("Source Network Offering"); @@ -1284,9 +1275,9 @@ public class ConfigurationManagerImplTest { public void testGetOrDefaultReturnsCommandValueWhenNotNull() { String cmdValue = "command-value"; String defaultValue = "default-value"; - + String result = configurationManagerImplSpy.getOrDefault(cmdValue, defaultValue); - + Assert.assertEquals(cmdValue, result); } @@ -1294,9 +1285,9 @@ public class ConfigurationManagerImplTest { public void testGetOrDefaultReturnsDefaultWhenCommandValueIsNull() { String cmdValue = null; String defaultValue = "default-value"; - + String result = configurationManagerImplSpy.getOrDefault(cmdValue, defaultValue); - + Assert.assertEquals(defaultValue, result); } @@ -1304,22 +1295,22 @@ public class ConfigurationManagerImplTest { public void testResolveBooleanParamUsesCommandValueWhenInRequestParams() { Map requestParams = new HashMap<>(); requestParams.put("offerha", "true"); - + Boolean result = configurationManagerImplSpy.resolveBooleanParam( requestParams, "offerha", () -> true, false ); - + Assert.assertTrue(result); } @Test public void testResolveBooleanParamUsesDefaultWhenNotInRequestParams() { Map requestParams = new HashMap<>(); - + Boolean result = configurationManagerImplSpy.resolveBooleanParam( requestParams, "offerha", () -> true, false ); - + Assert.assertFalse(result); } @@ -1328,7 +1319,7 @@ public class ConfigurationManagerImplTest { Boolean result = configurationManagerImplSpy.resolveBooleanParam( null, "offerha", () -> true, false ); - + Assert.assertFalse(result); } } diff --git a/ui/src/components/offering/ComputeOfferingForm.vue b/ui/src/components/offering/ComputeOfferingForm.vue new file mode 100644 index 00000000000..0efc0d5bbfe --- /dev/null +++ b/ui/src/components/offering/ComputeOfferingForm.vue @@ -0,0 +1,795 @@ + + + + + diff --git a/ui/src/components/offering/DiskOfferingForm.vue b/ui/src/components/offering/DiskOfferingForm.vue new file mode 100644 index 00000000000..b7d30168cf7 --- /dev/null +++ b/ui/src/components/offering/DiskOfferingForm.vue @@ -0,0 +1,507 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + + + + diff --git a/ui/src/config/section/offering.js b/ui/src/config/section/offering.js index a85d6fa6317..a65f3314a7c 100644 --- a/ui/src/config/section/offering.js +++ b/ui/src/config/section/offering.js @@ -408,7 +408,7 @@ export default { docHelp: 'adminguide/virtual_machines.html#importing-backup-offerings', dataView: true, popup: true, - component: shallowRef(defineAsyncComponent(() => import('@/views/offering/ImportBackupOffering.vue'))) + component: shallowRef(defineAsyncComponent(() => import('@/views/offering/CloneBackupOffering.vue'))) }, { api: 'deleteBackupOffering', icon: 'delete-outlined', diff --git a/ui/src/views/offering/AddComputeOffering.vue b/ui/src/views/offering/AddComputeOffering.vue index 465d07b8e57..72a63ef43f9 100644 --- a/ui/src/views/offering/AddComputeOffering.vue +++ b/ui/src/views/offering/AddComputeOffering.vue @@ -18,658 +18,29 @@ -