From d409852b4588b77f2388b55d4b1d74723dd247a3 Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Fri, 17 Apr 2026 13:44:35 +0200 Subject: [PATCH] NE: more unit tests and UI optimization --- .../java/com/cloud/network/NetworkTest.java | 17 ++ .../orchestration/NetworkOrchestrator.java | 2 +- .../manager/ExtensionsManagerImplTest.java | 230 ++++++++++++++++++ .../smoke/test_network_extension_namespace.py | 1 - ui/src/views/extension/AddCustomAction.vue | 6 +- .../views/extension/ExtensionResourcesTab.vue | 1 + ui/src/views/offering/AddNetworkOffering.vue | 103 ++------ ui/src/views/offering/AddVpcOffering.vue | 216 ++++------------ 8 files changed, 308 insertions(+), 268 deletions(-) diff --git a/api/src/test/java/com/cloud/network/NetworkTest.java b/api/src/test/java/com/cloud/network/NetworkTest.java index 9a937a4603d..dba4d1fb7eb 100644 --- a/api/src/test/java/com/cloud/network/NetworkTest.java +++ b/api/src/test/java/com/cloud/network/NetworkTest.java @@ -24,6 +24,7 @@ import java.util.List; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @@ -53,4 +54,20 @@ public class NetworkTest { Network.Provider transientProviderNew = Network.Provider.createTransientProvider("NetworkExtension"); assertTrue("List should contain the new transient provider with same name", providers.contains(transientProviderNew)); } + + @Test + public void testCustomActionServiceLookup() { + Network.Service customAction = Network.Service.getService("CustomAction"); + assertNotNull("CustomAction service should be available", customAction); + assertTrue("CustomAction should be part of the supported services list", + Network.Service.listAllServices().contains(customAction)); + } + + @Test + public void testTransientProviderIsNotGloballyRegistered() { + Network.Provider transientProvider = Network.Provider.createTransientProvider("TransientOnly"); + assertNotNull(transientProvider); + assertNull("Transient provider should not be retrievable from the global registry", + Network.Provider.getProvider("TransientOnly")); + } } diff --git a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java index 18d9d0786fe..de6b06b3205 100644 --- a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java +++ b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java @@ -4984,7 +4984,7 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra @Override public void expungeLbVmRefs(List vmIds, Long batchSize) { - if (CollectionUtils.isEmpty(getNetworkElementsIncludingExtensions()) || CollectionUtils.isEmpty(vmIds)) { + if (CollectionUtils.isEmpty(vmIds)) { return; } for (NetworkElement element : getNetworkElementsIncludingExtensions()) { diff --git a/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManagerImplTest.java b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManagerImplTest.java index 77feb1bad62..de2adf3e0c8 100644 --- a/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManagerImplTest.java +++ b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManagerImplTest.java @@ -40,6 +40,7 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.mockito.Mockito.withSettings; import java.io.File; import java.security.InvalidParameterException; @@ -136,6 +137,8 @@ import com.cloud.network.dao.PhysicalNetworkDao; import com.cloud.network.dao.PhysicalNetworkServiceProviderDao; import com.cloud.network.dao.PhysicalNetworkVO; import com.cloud.network.element.NetworkElement; +import com.cloud.network.vpc.Vpc; +import com.cloud.network.vpc.dao.VpcServiceMapDao; import org.apache.cloudstack.extension.NetworkCustomActionProvider; import com.cloud.org.Cluster; import com.cloud.serializer.GsonHelper; @@ -207,6 +210,8 @@ public class ExtensionsManagerImplTest { @Mock private NetworkServiceMapDao networkServiceMapDao; @Mock + private VpcServiceMapDao vpcServiceMapDao; + @Mock private NetworkModel networkModel; @Mock @@ -1830,6 +1835,231 @@ public class ExtensionsManagerImplTest { } } + @Test + public void runNetworkCustomAction_NoProviderFound_ReturnsFailureResponse() { + Network network = mock(Network.class); + when(network.getId()).thenReturn(11L); + + ExtensionCustomActionVO actionVO = mock(ExtensionCustomActionVO.class); + when(actionVO.getId()).thenReturn(10L); + when(actionVO.getUuid()).thenReturn("action-uuid"); + when(actionVO.getName()).thenReturn("dump-config"); + when(extensionCustomActionDetailsDao.listDetailsKeyPairsWithVisibility(10L)) + .thenReturn(new Pair<>(new HashMap<>(), new HashMap<>())); + + ExtensionVO extensionVO = mock(ExtensionVO.class); + + CustomActionResultResponse response = extensionsManager.runNetworkCustomAction( + network, actionVO, extensionVO, ExtensionCustomAction.ResourceType.Network, Collections.emptyMap()); + + assertFalse(response.getSuccess()); + assertEquals("No network service provider found for this network", response.getResult().get(ApiConstants.DETAILS)); + } + + @Test + public void runNetworkCustomAction_ProviderElementMissing_ReturnsFailureResponse() { + Network network = mock(Network.class); + when(network.getId()).thenReturn(12L); + when(networkServiceMapDao.getProviderForServiceInNetwork(12L, Network.Service.CustomAction)).thenReturn("ExtProvider"); + when(networkModel.getElementImplementingProvider("ExtProvider")).thenReturn(null); + + ExtensionCustomActionVO actionVO = mock(ExtensionCustomActionVO.class); + when(actionVO.getId()).thenReturn(11L); + when(actionVO.getUuid()).thenReturn("action-uuid"); + when(actionVO.getName()).thenReturn("dump-config"); + when(extensionCustomActionDetailsDao.listDetailsKeyPairsWithVisibility(11L)) + .thenReturn(new Pair<>(new HashMap<>(), new HashMap<>())); + + ExtensionVO extensionVO = mock(ExtensionVO.class); + + CustomActionResultResponse response = extensionsManager.runNetworkCustomAction( + network, actionVO, extensionVO, ExtensionCustomAction.ResourceType.Network, Collections.emptyMap()); + + assertFalse(response.getSuccess()); + assertEquals("No network element found for provider: ExtProvider", response.getResult().get(ApiConstants.DETAILS)); + } + + @Test + public void runNetworkCustomAction_ProviderCannotHandle_ReturnsFailureResponse() { + Network network = mock(Network.class); + when(network.getId()).thenReturn(13L); + when(networkServiceMapDao.getProviderForServiceInNetwork(13L, Network.Service.CustomAction)).thenReturn("ExtProvider"); + + NetworkElement element = mock(NetworkElement.class, withSettings().extraInterfaces(NetworkCustomActionProvider.class)); + NetworkCustomActionProvider provider = (NetworkCustomActionProvider) element; + when(networkModel.getElementImplementingProvider("ExtProvider")).thenReturn(element); + when(provider.canHandleCustomAction(network)).thenReturn(false); + + ExtensionCustomActionVO actionVO = mock(ExtensionCustomActionVO.class); + when(actionVO.getId()).thenReturn(12L); + when(actionVO.getUuid()).thenReturn("action-uuid"); + when(actionVO.getName()).thenReturn("dump-config"); + when(extensionCustomActionDetailsDao.listDetailsKeyPairsWithVisibility(12L)) + .thenReturn(new Pair<>(new HashMap<>(), new HashMap<>())); + + ExtensionVO extensionVO = mock(ExtensionVO.class); + + CustomActionResultResponse response = extensionsManager.runNetworkCustomAction( + network, actionVO, extensionVO, ExtensionCustomAction.ResourceType.Network, Collections.emptyMap()); + + assertFalse(response.getSuccess()); + assertTrue(response.getResult().get(ApiConstants.DETAILS).contains("cannot handle custom action")); + } + + @Test + public void runNetworkCustomAction_ProviderDoesNotImplementCustomAction_ReturnsFailureResponse() { + Network network = mock(Network.class); + when(network.getId()).thenReturn(131L); + when(networkServiceMapDao.getProviderForServiceInNetwork(131L, Network.Service.CustomAction)).thenReturn("ExtProvider"); + + NetworkElement element = mock(NetworkElement.class); + when(networkModel.getElementImplementingProvider("ExtProvider")).thenReturn(element); + + ExtensionCustomActionVO actionVO = mock(ExtensionCustomActionVO.class); + when(actionVO.getId()).thenReturn(121L); + when(actionVO.getUuid()).thenReturn("action-uuid"); + when(actionVO.getName()).thenReturn("dump-config"); + when(extensionCustomActionDetailsDao.listDetailsKeyPairsWithVisibility(121L)) + .thenReturn(new Pair<>(new HashMap<>(), new HashMap<>())); + + ExtensionVO extensionVO = mock(ExtensionVO.class); + + CustomActionResultResponse response = extensionsManager.runNetworkCustomAction( + network, actionVO, extensionVO, ExtensionCustomAction.ResourceType.Network, Collections.emptyMap()); + + assertFalse(response.getSuccess()); + assertTrue(response.getResult().get(ApiConstants.DETAILS).contains("does not support custom actions")); + } + + @Test + public void runNetworkCustomAction_SuccessfulExecution_ReturnsSuccessResponse() { + Network network = mock(Network.class); + when(network.getId()).thenReturn(14L); + when(networkServiceMapDao.getProviderForServiceInNetwork(14L, Network.Service.CustomAction)).thenReturn("ExtProvider"); + + NetworkElement element = mock(NetworkElement.class, withSettings().extraInterfaces(NetworkCustomActionProvider.class)); + NetworkCustomActionProvider provider = (NetworkCustomActionProvider) element; + when(networkModel.getElementImplementingProvider("ExtProvider")).thenReturn(element); + when(provider.canHandleCustomAction(network)).thenReturn(true); + when(provider.runCustomAction(eq(network), eq("dump-config"), any())).thenReturn("dump-output"); + + ExtensionCustomActionVO actionVO = mock(ExtensionCustomActionVO.class); + when(actionVO.getId()).thenReturn(13L); + when(actionVO.getUuid()).thenReturn("action-uuid"); + when(actionVO.getName()).thenReturn("dump-config"); + when(extensionCustomActionDetailsDao.listDetailsKeyPairsWithVisibility(13L)) + .thenReturn(new Pair<>(new HashMap<>(), new HashMap<>())); + + ExtensionVO extensionVO = mock(ExtensionVO.class); + + CustomActionResultResponse response = extensionsManager.runNetworkCustomAction( + network, actionVO, extensionVO, ExtensionCustomAction.ResourceType.Network, Collections.emptyMap()); + + assertTrue(response.getSuccess()); + assertEquals("dump-output", response.getResult().get(ApiConstants.DETAILS)); + } + + @Test + public void runVpcCustomAction_ProviderNotCustomActionProvider_ReturnsFailureResponse() { + Vpc vpc = mock(Vpc.class); + when(vpc.getId()).thenReturn(21L); + when(vpcServiceMapDao.getProviderForServiceInVpc(21L, Network.Service.CustomAction)).thenReturn("VpcProvider"); + + NetworkElement element = mock(NetworkElement.class); + when(networkModel.getElementImplementingProvider("VpcProvider")).thenReturn(element); + + ExtensionCustomActionVO actionVO = mock(ExtensionCustomActionVO.class); + when(actionVO.getId()).thenReturn(20L); + when(actionVO.getUuid()).thenReturn("action-uuid"); + when(actionVO.getName()).thenReturn("dump-config"); + when(extensionCustomActionDetailsDao.listDetailsKeyPairsWithVisibility(20L)) + .thenReturn(new Pair<>(new HashMap<>(), new HashMap<>())); + + ExtensionVO extensionVO = mock(ExtensionVO.class); + + CustomActionResultResponse response = extensionsManager.runVpcCustomAction( + vpc, actionVO, extensionVO, ExtensionCustomAction.ResourceType.Vpc, Collections.emptyMap()); + + assertFalse(response.getSuccess()); + assertTrue(response.getResult().get(ApiConstants.DETAILS).contains("does not support custom actions")); + } + + @Test + public void runVpcCustomAction_NoProviderFound_ReturnsFailureResponse() { + Vpc vpc = mock(Vpc.class); + when(vpc.getId()).thenReturn(211L); + + ExtensionCustomActionVO actionVO = mock(ExtensionCustomActionVO.class); + when(actionVO.getId()).thenReturn(201L); + when(actionVO.getUuid()).thenReturn("action-uuid"); + when(actionVO.getName()).thenReturn("dump-config"); + when(extensionCustomActionDetailsDao.listDetailsKeyPairsWithVisibility(201L)) + .thenReturn(new Pair<>(new HashMap<>(), new HashMap<>())); + + ExtensionVO extensionVO = mock(ExtensionVO.class); + + CustomActionResultResponse response = extensionsManager.runVpcCustomAction( + vpc, actionVO, extensionVO, ExtensionCustomAction.ResourceType.Vpc, Collections.emptyMap()); + + assertFalse(response.getSuccess()); + assertEquals("No VPC service provider found for this VPC", response.getResult().get(ApiConstants.DETAILS)); + } + + @Test + public void runVpcCustomAction_ProviderCannotHandleVpc_ReturnsFailureResponse() { + Vpc vpc = mock(Vpc.class); + when(vpc.getId()).thenReturn(212L); + when(vpcServiceMapDao.getProviderForServiceInVpc(212L, Network.Service.CustomAction)).thenReturn("VpcProvider"); + + NetworkElement element = mock(NetworkElement.class, withSettings().extraInterfaces(NetworkCustomActionProvider.class)); + NetworkCustomActionProvider provider = (NetworkCustomActionProvider) element; + when(networkModel.getElementImplementingProvider("VpcProvider")).thenReturn(element); + when(provider.canHandleVpcCustomAction(vpc)).thenReturn(false); + + ExtensionCustomActionVO actionVO = mock(ExtensionCustomActionVO.class); + when(actionVO.getId()).thenReturn(202L); + when(actionVO.getUuid()).thenReturn("action-uuid"); + when(actionVO.getName()).thenReturn("dump-config"); + when(extensionCustomActionDetailsDao.listDetailsKeyPairsWithVisibility(202L)) + .thenReturn(new Pair<>(new HashMap<>(), new HashMap<>())); + + ExtensionVO extensionVO = mock(ExtensionVO.class); + + CustomActionResultResponse response = extensionsManager.runVpcCustomAction( + vpc, actionVO, extensionVO, ExtensionCustomAction.ResourceType.Vpc, Collections.emptyMap()); + + assertFalse(response.getSuccess()); + assertTrue(response.getResult().get(ApiConstants.DETAILS).contains("cannot handle custom action")); + } + + @Test + public void runVpcCustomAction_SuccessfulExecution_ReturnsSuccessResponse() { + Vpc vpc = mock(Vpc.class); + when(vpc.getId()).thenReturn(22L); + when(vpcServiceMapDao.getProviderForServiceInVpc(22L, Network.Service.CustomAction)).thenReturn("VpcProvider"); + + NetworkElement element = mock(NetworkElement.class, withSettings().extraInterfaces(NetworkCustomActionProvider.class)); + NetworkCustomActionProvider provider = (NetworkCustomActionProvider) element; + when(networkModel.getElementImplementingProvider("VpcProvider")).thenReturn(element); + when(provider.canHandleVpcCustomAction(vpc)).thenReturn(true); + when(provider.runCustomAction(eq(vpc), eq("dump-config"), any())).thenReturn("vpc-dump-output"); + + ExtensionCustomActionVO actionVO = mock(ExtensionCustomActionVO.class); + when(actionVO.getId()).thenReturn(21L); + when(actionVO.getUuid()).thenReturn("action-uuid"); + when(actionVO.getName()).thenReturn("dump-config"); + when(extensionCustomActionDetailsDao.listDetailsKeyPairsWithVisibility(21L)) + .thenReturn(new Pair<>(new HashMap<>(), new HashMap<>())); + + ExtensionVO extensionVO = mock(ExtensionVO.class); + + CustomActionResultResponse response = extensionsManager.runVpcCustomAction( + vpc, actionVO, extensionVO, ExtensionCustomAction.ResourceType.Vpc, Collections.emptyMap()); + + assertTrue(response.getSuccess()); + assertEquals("vpc-dump-output", response.getResult().get(ApiConstants.DETAILS)); + } + @Test public void createCustomActionResponse_SetsBasicFields() { ExtensionCustomAction action = mock(ExtensionCustomAction.class); diff --git a/test/integration/smoke/test_network_extension_namespace.py b/test/integration/smoke/test_network_extension_namespace.py index be3ed29163f..20880fcba35 100644 --- a/test/integration/smoke/test_network_extension_namespace.py +++ b/test/integration/smoke/test_network_extension_namespace.py @@ -2839,4 +2839,3 @@ class TestNetworkExtensionNamespace(cloudstackTestCase): self._teardown_extension() except Exception: pass - diff --git a/ui/src/views/extension/AddCustomAction.vue b/ui/src/views/extension/AddCustomAction.vue index 7c1de26b504..160e1efa8f6 100644 --- a/ui/src/views/extension/AddCustomAction.vue +++ b/ui/src/views/extension/AddCustomAction.vue @@ -169,7 +169,7 @@ export default { data () { return { roleTypes: [], - resourceTypeOptions: ['VirtualMachine', 'Network'], + resourceTypeOptions: ['VirtualMachine', 'Network', 'Vpc'], loading: false } }, @@ -204,13 +204,13 @@ export default { updateResourceTypeByExtension (selectedExtension) { const type = selectedExtension?.type if (type === 'NetworkOrchestrator') { - this.resourceTypeOptions = ['Network'] + this.resourceTypeOptions = ['Network', 'Vpc'] this.form.resourcetype = 'Network' } else if (type === 'Orchestrator') { this.resourceTypeOptions = ['VirtualMachine'] this.form.resourcetype = 'VirtualMachine' } else { - this.resourceTypeOptions = ['VirtualMachine', 'Network'] + this.resourceTypeOptions = ['VirtualMachine', 'Network', 'Vpc'] if (!this.form.resourcetype) { this.form.resourcetype = 'VirtualMachine' } diff --git a/ui/src/views/extension/ExtensionResourcesTab.vue b/ui/src/views/extension/ExtensionResourcesTab.vue index 503c06629c0..6c577e66584 100644 --- a/ui/src/views/extension/ExtensionResourcesTab.vue +++ b/ui/src/views/extension/ExtensionResourcesTab.vue @@ -36,6 +36,7 @@