diff --git a/api/src/main/java/com/cloud/event/EventTypes.java b/api/src/main/java/com/cloud/event/EventTypes.java
index 42395bf89992..f3c3fabc06fd 100644
--- a/api/src/main/java/com/cloud/event/EventTypes.java
+++ b/api/src/main/java/com/cloud/event/EventTypes.java
@@ -854,6 +854,7 @@ public class EventTypes {
public static final String EVENT_EXTENSION_DELETE = "EXTENSION.DELETE";
public static final String EVENT_EXTENSION_RESOURCE_REGISTER = "EXTENSION.RESOURCE.REGISTER";
public static final String EVENT_EXTENSION_RESOURCE_UNREGISTER = "EXTENSION.RESOURCE.UNREGISTER";
+ public static final String EVENT_EXTENSION_RESOURCE_UPDATE = "EXTENSION.RESOURCE.UPDATE";
public static final String EVENT_EXTENSION_CUSTOM_ACTION_ADD = "EXTENSION.CUSTOM.ACTION.ADD";
public static final String EVENT_EXTENSION_CUSTOM_ACTION_UPDATE = "EXTENSION.CUSTOM.ACTION.UPDATE";
public static final String EVENT_EXTENSION_CUSTOM_ACTION_DELETE = "EXTENSION.CUSTOM.ACTION.DELETE";
diff --git a/api/src/main/java/com/cloud/network/Network.java b/api/src/main/java/com/cloud/network/Network.java
index 0846306f70f9..55cb755cc161 100644
--- a/api/src/main/java/com/cloud/network/Network.java
+++ b/api/src/main/java/com/cloud/network/Network.java
@@ -116,6 +116,7 @@ class Service {
public static final Service NetworkACL = new Service("NetworkACL", Capability.SupportedProtocols);
public static final Service Connectivity = new Service("Connectivity", Capability.DistributedRouter, Capability.RegionLevelVpc, Capability.StretchedL2Subnet,
Capability.NoVlan, Capability.PublicAccess);
+ public static final Service CustomAction = new Service("CustomAction");
private final String name;
private final Capability[] caps;
@@ -207,6 +208,7 @@ public static class Provider {
public static final Provider Nsx = new Provider("Nsx", false);
public static final Provider Netris = new Provider("Netris", false);
+ public static final Provider NetworkExtension = new Provider("NetworkExtension", false, true);
private final String name;
private final boolean isExternal;
@@ -250,11 +252,47 @@ public static Provider getProvider(String providerName) {
return null;
}
+ /** Private constructor for transient (non-registered) providers. */
+ private Provider(String name) {
+ this.name = name;
+ this.isExternal = false;
+ this.needCleanupOnShutdown = true;
+ // intentionally NOT added to supportedProviders
+ }
+
+ /**
+ * Creates a transient (non-registered) {@link Provider} with the given name.
+ *
+ *
The new instance is not added to {@code supportedProviders}, so it
+ * will never be returned by {@link #getProvider(String)} and will not pollute the
+ * global provider registry. Use this for dynamic / extension-backed providers
+ * whose names are only known at runtime (e.g. NetworkOrchestrator extensions).
+ *
+ * @param name the provider name (typically the extension name)
+ * @return a transient {@link Provider} instance with the given name
+ */
+ public static Provider createTransientProvider(String name) {
+ return new Provider(name);
+ }
+
@Override public String toString() {
return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
.append("name", name)
.toString();
}
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+ if (!(obj instanceof Provider)) return false;
+ Provider provider = (Provider) obj;
+ return this.name.equals(provider.name);
+ }
+
+ @Override
+ public int hashCode() {
+ return name.hashCode();
+ }
}
public static class Capability {
diff --git a/api/src/main/java/com/cloud/network/NetworkModel.java b/api/src/main/java/com/cloud/network/NetworkModel.java
index c212e6319eb4..7e1a07ebeb69 100644
--- a/api/src/main/java/com/cloud/network/NetworkModel.java
+++ b/api/src/main/java/com/cloud/network/NetworkModel.java
@@ -187,6 +187,8 @@ public interface NetworkModel {
boolean canElementEnableIndividualServices(Provider provider);
+ boolean canElementEnableIndividualServicesByName(String providerName);
+
boolean areServicesSupportedInNetwork(long networkId, Service... services);
boolean isNetworkSystem(Network network);
@@ -237,6 +239,18 @@ public interface NetworkModel {
String getDefaultGuestTrafficLabel(long dcId, HypervisorType vmware);
+ /**
+ * Resolves a provider name to a {@link Provider} instance.
+ * For known static providers, delegates to {@link Provider#getProvider(String)}.
+ * For dynamically-registered NetworkOrchestrator extension providers whose names
+ * are not in the static registry, returns a transient {@link Provider} with the
+ * given name so callers can still dispatch correctly.
+ *
+ * @param providerName the provider name from {@code ntwk_service_map} or similar
+ * @return a {@link Provider} instance, or {@code null} if not resolvable
+ */
+ Provider resolveProvider(String providerName);
+
/**
* @param providerName
* @return
diff --git a/api/src/main/java/com/cloud/network/element/NetworkElement.java b/api/src/main/java/com/cloud/network/element/NetworkElement.java
index cb0fc2fca981..67be7b9ba2e2 100644
--- a/api/src/main/java/com/cloud/network/element/NetworkElement.java
+++ b/api/src/main/java/com/cloud/network/element/NetworkElement.java
@@ -146,4 +146,8 @@ boolean shutdownProviderInstances(PhysicalNetworkServiceProvider provider, Reser
* @return true/false
*/
boolean verifyServicesCombination(Set services);
+
+ default boolean rollingRestartSupported() {
+ return true;
+ }
}
diff --git a/api/src/main/java/org/apache/cloudstack/extension/CustomActionResultResponse.java b/api/src/main/java/org/apache/cloudstack/extension/CustomActionResultResponse.java
index 33ff70fcace5..558340694b8a 100644
--- a/api/src/main/java/org/apache/cloudstack/extension/CustomActionResultResponse.java
+++ b/api/src/main/java/org/apache/cloudstack/extension/CustomActionResultResponse.java
@@ -62,4 +62,12 @@ public Boolean getSuccess() {
public void setResult(Map result) {
this.result = result;
}
+
+ public Map getResult() {
+ return result;
+ }
+
+ public boolean isSuccess() {
+ return Boolean.TRUE.equals(success);
+ }
}
diff --git a/api/src/main/java/org/apache/cloudstack/extension/Extension.java b/api/src/main/java/org/apache/cloudstack/extension/Extension.java
index 3068612ed6fe..c1d905718b24 100644
--- a/api/src/main/java/org/apache/cloudstack/extension/Extension.java
+++ b/api/src/main/java/org/apache/cloudstack/extension/Extension.java
@@ -24,7 +24,8 @@
public interface Extension extends InternalIdentity, Identity {
enum Type {
- Orchestrator
+ Orchestrator,
+ NetworkOrchestrator
}
enum State {
Enabled, Disabled;
diff --git a/api/src/main/java/org/apache/cloudstack/extension/ExtensionCustomAction.java b/api/src/main/java/org/apache/cloudstack/extension/ExtensionCustomAction.java
index 776b42f671b7..245faa762a2f 100644
--- a/api/src/main/java/org/apache/cloudstack/extension/ExtensionCustomAction.java
+++ b/api/src/main/java/org/apache/cloudstack/extension/ExtensionCustomAction.java
@@ -48,7 +48,9 @@
public interface ExtensionCustomAction extends InternalIdentity, Identity {
enum ResourceType {
- VirtualMachine(com.cloud.vm.VirtualMachine.class);
+ VirtualMachine(com.cloud.vm.VirtualMachine.class),
+ Network(com.cloud.network.Network.class),
+ Vpc(com.cloud.network.vpc.Vpc.class);
private final Class> clazz;
diff --git a/api/src/main/java/org/apache/cloudstack/extension/ExtensionHelper.java b/api/src/main/java/org/apache/cloudstack/extension/ExtensionHelper.java
index a01131278a76..2c373988feac 100644
--- a/api/src/main/java/org/apache/cloudstack/extension/ExtensionHelper.java
+++ b/api/src/main/java/org/apache/cloudstack/extension/ExtensionHelper.java
@@ -18,10 +18,99 @@
package org.apache.cloudstack.extension;
import java.util.List;
+import java.util.Map;
+
+import com.cloud.network.Network.Capability;
+import com.cloud.network.Network.Service;
public interface ExtensionHelper {
Long getExtensionIdForCluster(long clusterId);
Extension getExtension(long id);
Extension getExtensionForCluster(long clusterId);
List getExtensionReservedResourceDetails(long extensionId);
+
+ /**
+ * Detail key used to store the comma-separated list of network services provided
+ * by a NetworkOrchestrator extension (e.g. {@code "SourceNat,StaticNat,Firewall"}).
+ */
+ String NETWORK_SERVICES_DETAIL_KEY = "network.services";
+
+ /**
+ * Detail key used to store a JSON object mapping each service name to its
+ * CloudStack {@link com.cloud.network.Network.Capability} key/value pairs.
+ * Example: {@code {"SourceNat":{"SupportedSourceNatTypes":"peraccount"}}}.
+ * Used together with {@link #NETWORK_SERVICES_DETAIL_KEY}.
+ */
+ String NETWORK_SERVICE_CAPABILITIES_DETAIL_KEY = "network.service.capabilities";
+
+ Long getExtensionIdForPhysicalNetwork(long physicalNetworkId);
+ Extension getExtensionForPhysicalNetwork(long physicalNetworkId);
+ String getExtensionScriptPath(Extension extension);
+ Map getExtensionDetails(long extensionId);
+
+ /**
+ * Finds the extension registered with the given physical network whose name
+ * matches the given provider name (case-insensitive). Returns {@code null}
+ * if no matching extension is found.
+ *
+ * This is the preferred lookup when multiple extensions are registered on
+ * the same physical network: the provider name stored in
+ * {@code ntwk_service_map} is used to pinpoint the exact extension that
+ * handles a given network.
+ *
+ * @param physicalNetworkId the physical network ID
+ * @param providerName the provider name (must equal the extension name)
+ * @return the matching {@link Extension}, or {@code null}
+ */
+ Extension getExtensionForPhysicalNetworkAndProvider(long physicalNetworkId, String providerName);
+
+ /**
+ * Returns ALL {@code extension_resource_map_details} (including hidden) for
+ * the specific extension registered on the given physical network. Used by
+ * {@code NetworkExtensionElement} to inject device credentials into the script
+ * environment for the correct extension when multiple different extensions are
+ * registered on the same physical network.
+ *
+ * @param physicalNetworkId the physical network ID
+ * @param extensionId the extension ID
+ * @return all key/value details including non-display ones, or an empty map
+ */
+ Map getAllResourceMapDetailsForExtensionOnPhysicalNetwork(long physicalNetworkId, long extensionId);
+
+ /**
+ * Returns {@code true} if the given provider name is backed by a
+ * {@code NetworkOrchestrator} extension registered on any physical network.
+ * This is used by {@code NetworkModelImpl} to detect extension-backed providers
+ * that are not in the static {@code s_providerToNetworkElementMap}.
+ *
+ * @param providerName the provider / extension name
+ * @return true if the provider is a NetworkExtension provider
+ */
+ boolean isNetworkExtensionProvider(String providerName);
+
+ /**
+ * List all registered extensions filtered by extension {@link Extension.Type}.
+ * Useful for callers that need to discover available providers of a given
+ * type (e.g. Orchestrator, NetworkOrchestrator).
+ *
+ * @param type extension type to filter by
+ * @return list of matching {@link Extension} instances (empty list if none)
+ */
+ List listExtensionsByType(Extension.Type type);
+
+ /**
+ * Returns the effective {@link Service} → ({@link Capability} → value) capabilities
+ * for the given external network provider, looking it up by name on the given
+ * physical network.
+ *
+ * If {@code physicalNetworkId} is {@code null}, the method searches across all
+ * physical networks that have extensions registered and returns the capabilities for
+ * the first matching extension.
+ *
+ * @param physicalNetworkId physical network ID, or {@code null} for offering-level queries
+ * @param providerName provider / extension name
+ * @return capabilities map, or the default capabilities if no matching extension is found
+ */
+ Map> getNetworkCapabilitiesForProvider(Long physicalNetworkId, String providerName);
+
}
diff --git a/api/src/main/java/org/apache/cloudstack/extension/ExtensionResourceMap.java b/api/src/main/java/org/apache/cloudstack/extension/ExtensionResourceMap.java
index 40ebc19eb5e3..604e64a1f894 100644
--- a/api/src/main/java/org/apache/cloudstack/extension/ExtensionResourceMap.java
+++ b/api/src/main/java/org/apache/cloudstack/extension/ExtensionResourceMap.java
@@ -24,7 +24,8 @@
public interface ExtensionResourceMap extends InternalIdentity, Identity {
enum ResourceType {
- Cluster
+ Cluster,
+ PhysicalNetwork
}
long getExtensionId();
diff --git a/api/src/main/java/org/apache/cloudstack/extension/NetworkCustomActionProvider.java b/api/src/main/java/org/apache/cloudstack/extension/NetworkCustomActionProvider.java
new file mode 100644
index 000000000000..1c281c15f286
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/extension/NetworkCustomActionProvider.java
@@ -0,0 +1,74 @@
+// 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.
+
+package org.apache.cloudstack.extension;
+
+import java.util.Map;
+
+import com.cloud.network.Network;
+import com.cloud.network.vpc.Vpc;
+
+/**
+ * Implemented by network elements that support running custom actions on a
+ * managed network or VPC (e.g. NetworkExtensionElement).
+ *
+ * This interface is looked up by {@code ExtensionsManagerImpl} to dispatch
+ * {@code runCustomAction} requests whose resource type is {@code Network}
+ * or {@code Vpc}.
+ */
+public interface NetworkCustomActionProvider {
+
+ /**
+ * Returns {@code true} if this provider handles networks whose physical
+ * network has an ExternalNetwork service provider registered.
+ *
+ * @param network the target network
+ * @return {@code true} if this provider can handle the network
+ */
+ boolean canHandleCustomAction(Network network);
+
+ /**
+ * Returns {@code true} if this provider can handle custom actions for
+ * the given VPC.
+ *
+ * @param vpc the target VPC
+ * @return {@code true} if this provider can handle the VPC
+ */
+ boolean canHandleVpcCustomAction(Vpc vpc);
+
+ /**
+ * Runs a named custom action against the external network device that
+ * manages the given network.
+ *
+ * @param network the CloudStack network on which to run the action
+ * @param actionName the action name (e.g. {@code "reboot-device"}, {@code "dump-config"})
+ * @param parameters optional parameters supplied by the caller
+ * @return output from the action script, or {@code null} on failure
+ */
+ String runCustomAction(Network network, String actionName, Map parameters);
+
+ /**
+ * Runs a named custom action against the external network device that
+ * manages the given VPC.
+ *
+ * @param vpc the CloudStack VPC on which to run the action
+ * @param actionName the action name
+ * @param parameters optional parameters supplied by the caller
+ * @return output from the action script, or {@code null} on failure
+ */
+ String runCustomAction(Vpc vpc, String actionName, Map parameters);
+}
diff --git a/api/src/test/java/com/cloud/network/NetworkTest.java b/api/src/test/java/com/cloud/network/NetworkTest.java
new file mode 100644
index 000000000000..dba4d1fb7eb5
--- /dev/null
+++ b/api/src/test/java/com/cloud/network/NetworkTest.java
@@ -0,0 +1,73 @@
+// 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.
+
+package com.cloud.network;
+
+import org.junit.Test;
+
+import java.util.ArrayList;
+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;
+
+
+public class NetworkTest {
+
+ @Test
+ public void testProviderContains() {
+ List providers = new ArrayList<>();
+ providers.add(Network.Provider.VirtualRouter);
+
+ // direct instance present
+ assertTrue("List should contain VirtualRouter provider", providers.contains(Network.Provider.VirtualRouter));
+
+ // resolved provider by name (registered provider)
+ Network.Provider resolved = Network.Provider.getProvider("VirtualRouter");
+ assertNotNull("Resolved provider should not be null", resolved);
+ assertTrue("List should contain resolved VirtualRouter provider", providers.contains(resolved));
+
+ // transient provider with same name should be considered equal (equals by name)
+ Network.Provider transientProvider = Network.Provider.createTransientProvider("NetworkExtension");
+ assertFalse("List should not contain the transient provider", providers.contains(transientProvider));
+
+ providers.add(transientProvider);
+ assertTrue("List should contain the transient provider", providers.contains(transientProvider));
+
+ // another transient provider with same name should be considered equal
+ 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/api/src/test/java/org/apache/cloudstack/extension/CustomActionResultResponseTest.java b/api/src/test/java/org/apache/cloudstack/extension/CustomActionResultResponseTest.java
new file mode 100644
index 000000000000..9b7346f332cb
--- /dev/null
+++ b/api/src/test/java/org/apache/cloudstack/extension/CustomActionResultResponseTest.java
@@ -0,0 +1,92 @@
+// 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.
+
+package org.apache.cloudstack.extension;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Map;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class CustomActionResultResponseTest {
+
+ private CustomActionResultResponse response;
+
+ @Before
+ public void setUp() {
+ response = new CustomActionResultResponse();
+ }
+
+ @Test
+ public void getResultReturnsNullByDefault() {
+ assertNull(response.getResult());
+ }
+
+ @Test
+ public void getResultReturnsSetValue() {
+ Map result = Map.of("message", "OK", "details", "All good");
+ response.setResult(result);
+ assertEquals(result, response.getResult());
+ assertEquals("OK", response.getResult().get("message"));
+ }
+
+ @Test
+ public void isSuccessReturnsFalseWhenSuccessIsNull() {
+ // success is null by default
+ assertFalse(response.isSuccess());
+ }
+
+ @Test
+ public void isSuccessReturnsFalseWhenSuccessIsFalse() {
+ response.setSuccess(false);
+ assertFalse(response.isSuccess());
+ }
+
+ @Test
+ public void isSuccessReturnsTrueWhenSuccessIsTrue() {
+ response.setSuccess(true);
+ assertTrue(response.isSuccess());
+ }
+
+ @Test
+ public void getSuccessReturnsNullByDefault() {
+ assertNull(response.getSuccess());
+ }
+
+ @Test
+ public void getSuccessReturnsTrueAfterSetSuccessTrue() {
+ response.setSuccess(true);
+ assertTrue(response.getSuccess());
+ }
+
+ @Test
+ public void getSuccessReturnsFalseAfterSetSuccessFalse() {
+ response.setSuccess(false);
+ assertFalse(response.getSuccess());
+ }
+
+ @Test
+ public void setAndGetResultWithNullResult() {
+ response.setResult(null);
+ assertNull(response.getResult());
+ }
+}
diff --git a/api/src/test/java/org/apache/cloudstack/extension/ExtensionCustomActionTest.java b/api/src/test/java/org/apache/cloudstack/extension/ExtensionCustomActionTest.java
index ae4314aa11a8..a6bd49a2d8be 100644
--- a/api/src/test/java/org/apache/cloudstack/extension/ExtensionCustomActionTest.java
+++ b/api/src/test/java/org/apache/cloudstack/extension/ExtensionCustomActionTest.java
@@ -40,6 +40,12 @@ public void testResourceType() {
assertEquals(com.cloud.vm.VirtualMachine.class, vmType.getAssociatedClass());
}
+ @Test
+ public void testNetworkResourceType() {
+ ExtensionCustomAction.ResourceType networkType = ExtensionCustomAction.ResourceType.Network;
+ assertEquals(com.cloud.network.Network.class, networkType.getAssociatedClass());
+ }
+
@Test
public void testParameterTypeSupportsOptions() {
assertTrue(ExtensionCustomAction.Parameter.Type.STRING.canSupportsOptions());
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 7d455e7d6dc9..16102c5b8502 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
@@ -177,6 +177,10 @@
import com.cloud.network.dao.RemoteAccessVpnDao;
import com.cloud.network.dao.RemoteAccessVpnVO;
import com.cloud.network.dao.RouterNetworkDao;
+import org.apache.cloudstack.extension.Extension;
+import org.apache.cloudstack.extension.ExtensionHelper;
+import org.apache.cloudstack.framework.extensions.network.NetworkExtensionElement;
+
import com.cloud.network.element.AggregatedCommandExecutor;
import com.cloud.network.element.ConfigDriveNetworkElement;
import com.cloud.network.element.DhcpServiceProvider;
@@ -368,6 +372,10 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra
private BGPService bgpService;
@Inject
private Ipv6GuestPrefixSubnetNetworkMapDao ipv6GuestPrefixSubnetNetworkMapDao;
+ @Inject
+ protected ExtensionHelper extensionHelper;
+ @Inject
+ private NetworkExtensionElement networkExtensionElement;
@Override
public List getNetworkGurus() {
@@ -461,6 +469,28 @@ public void setDhcpProviders(final List dhcpProviders) {
HashMap _lastNetworkIdsToFree = new HashMap<>();
+ /**
+ * Returns the full list of network elements to iterate when implementing,
+ * shutting down, or otherwise orchestrating a network.
+ *
+ * The base list ({@link #networkElements}, wired by Spring) is extended
+ * at runtime with one transient {@link NetworkExtensionElement} per
+ * registered {@code NetworkOrchestrator} extension. This keeps the
+ * Spring bean list free from {@code NetworkExtensionElement} and allows
+ * dynamic discovery of extensions without a restart.
+ */
+ private List getNetworkElementsIncludingExtensions() {
+ List extensions = extensionHelper.listExtensionsByType(Extension.Type.NetworkOrchestrator);
+ if (extensions == null || extensions.isEmpty()) {
+ return networkElements;
+ }
+ List combined = new ArrayList<>(networkElements);
+ for (Extension ext : extensions) {
+ combined.add(networkExtensionElement.withProviderName(ext.getName()));
+ }
+ return combined;
+ }
+
private void updateRouterDefaultDns(final VirtualMachineProfile vmProfile, final NicProfile nicProfile) {
if (!Type.DomainRouter.equals(vmProfile.getType()) || !nicProfile.isDefaultNic()) {
return;
@@ -1685,7 +1715,7 @@ public void implementNetworkElementsAndResources(final DeployDestination dest, f
}
}
- for (final NetworkElement element : networkElements) {
+ for (final NetworkElement element : getNetworkElementsIncludingExtensions()) {
if (element instanceof AggregatedCommandExecutor && providersToImplement.contains(element.getProvider())) {
((AggregatedCommandExecutor) element).prepareAggregatedExecution(network, dest);
}
@@ -1702,7 +1732,7 @@ public void implementNetworkElementsAndResources(final DeployDestination dest, f
ex.addProxyObject(_entityMgr.findById(DataCenter.class, network.getDataCenterId()).getUuid());
throw ex;
}
- for (final NetworkElement element : networkElements) {
+ for (final NetworkElement element : getNetworkElementsIncludingExtensions()) {
if (element instanceof AggregatedCommandExecutor && providersToImplement.contains(element.getProvider())) {
if (!((AggregatedCommandExecutor) element).completeAggregatedExecution(network, dest)) {
logger.warn("Failed to re-program the network as a part of network {} implement due to aggregated commands execution failure!", network);
@@ -1716,7 +1746,7 @@ public void implementNetworkElementsAndResources(final DeployDestination dest, f
}
reconfigureAndApplyStaticRouteForVpcVpn(network);
} finally {
- for (final NetworkElement element : networkElements) {
+ for (final NetworkElement element : getNetworkElementsIncludingExtensions()) {
if (element instanceof AggregatedCommandExecutor && providersToImplement.contains(element.getProvider())) {
((AggregatedCommandExecutor) element).cleanupAggregatedExecution(network, dest);
}
@@ -1737,7 +1767,7 @@ private void reconfigureAndApplyStaticRouteForVpcVpn(Network network) {
private void implementNetworkElements(final DeployDestination dest, final ReservationContext context, final Network network, final NetworkOffering offering, final List providersToImplement)
throws ConcurrentOperationException, ResourceUnavailableException, InsufficientCapacityException {
- for (NetworkElement element : networkElements) {
+ for (NetworkElement element : getNetworkElementsIncludingExtensions()) {
if (providersToImplement.contains(element.getProvider())) {
if (!_networkModel.isProviderEnabledInPhysicalNetwork(_networkModel.getPhysicalNetworkId(network), element.getProvider().getName())) {
// The physicalNetworkId will not get translated into a uuid by the response serializer,
@@ -2030,7 +2060,7 @@ public void doInTransactionWithoutResult(TransactionStatus status) {
@Override
public void configureUpdateInSequence(Network network) {
List providers = getNetworkProviders(network.getId());
- for (NetworkElement element : networkElements) {
+ for (NetworkElement element : getNetworkElementsIncludingExtensions()) {
if (providers.contains(element.getProvider())) {
if (element instanceof RedundantResource) {
((RedundantResource) element).configureResource(network);
@@ -2043,7 +2073,7 @@ public void configureUpdateInSequence(Network network) {
public int getResourceCount(Network network) {
List providers = getNetworkProviders(network.getId());
int resourceCount = 0;
- for (NetworkElement element : networkElements) {
+ for (NetworkElement element : getNetworkElementsIncludingExtensions()) {
if (providers.contains(element.getProvider())) {
//currently only one element implements the redundant resource interface
if (element instanceof RedundantResource) {
@@ -2074,7 +2104,7 @@ public void configureExtraDhcpOptions(Network network, long nicId) {
@Override
public void finalizeUpdateInSequence(Network network, boolean success) {
List providers = getNetworkProviders(network.getId());
- for (NetworkElement element : networkElements) {
+ for (NetworkElement element : getNetworkElementsIncludingExtensions()) {
if (providers.contains(element.getProvider())) {
//currently only one element implements the redundant resource interface
if (element instanceof RedundantResource) {
@@ -2101,7 +2131,7 @@ public void setHypervisorHostname(VirtualMachineProfile vm, DeployDestination de
}
private void setHypervisorHostnameInNetwork(VirtualMachineProfile vm, DeployDestination dest, Network network, NicProfile profile, boolean migrationSuccessful) {
- for (final NetworkElement element : networkElements) {
+ for (final NetworkElement element : getNetworkElementsIncludingExtensions()) {
if (_networkModel.areServicesSupportedInNetwork(network.getId(), Service.UserData) && element instanceof UserDataServiceProvider
&& (element instanceof ConfigDriveNetworkElement && !migrationSuccessful || element instanceof VirtualRouterElement && migrationSuccessful)) {
String errorMsg = String.format("Failed to add hypervisor host name while applying the userdata during the migration of VM %s, " +
@@ -2229,7 +2259,7 @@ public NicProfile prepareNic(final VirtualMachineProfile vmProfile, final Deploy
updateNic(nic, network, 1);
final List providersToImplement = getNetworkProviders(network.getId());
- for (final NetworkElement element : networkElements) {
+ for (final NetworkElement element : getNetworkElementsIncludingExtensions()) {
if (providersToImplement.contains(element.getProvider())) {
if (!_networkModel.isProviderEnabledInPhysicalNetwork(_networkModel.getPhysicalNetworkId(network), element.getProvider().getName())) {
throw new CloudRuntimeException("Service provider " + element.getProvider().getName() + " either doesn't exist or is not enabled in physical network id: "
@@ -2284,7 +2314,7 @@ public void prepareNicForMigration(final VirtualMachineProfile vm, final DeployD
}
final List providersToImplement = getNetworkProviders(network.getId());
- for (final NetworkElement element : networkElements) {
+ for (final NetworkElement element : getNetworkElementsIncludingExtensions()) {
if (providersToImplement.contains(element.getProvider())) {
if (!_networkModel.isProviderEnabledInPhysicalNetwork(_networkModel.getPhysicalNetworkId(network), element.getProvider().getName())) {
throw new CloudRuntimeException("Service provider " + element.getProvider().getName() + " either doesn't exist or is not enabled in physical network id: "
@@ -2328,7 +2358,7 @@ public void prepareAllNicsForMigration(final VirtualMachineProfile vm, final Dep
}
}
final List providersToImplement = getNetworkProviders(network.getId());
- for (final NetworkElement element : networkElements) {
+ for (final NetworkElement element : getNetworkElementsIncludingExtensions()) {
if (providersToImplement.contains(element.getProvider())) {
if (!_networkModel.isProviderEnabledInPhysicalNetwork(_networkModel.getPhysicalNetworkId(network), element.getProvider().getName())) {
throw new CloudRuntimeException(String.format("Service provider %s either doesn't exist or is not enabled in physical network: %s",
@@ -2410,7 +2440,7 @@ public void commitNicForMigration(final VirtualMachineProfile src, final Virtual
}
final List providersToImplement = getNetworkProviders(network.getId());
- for (final NetworkElement element : networkElements) {
+ for (final NetworkElement element : getNetworkElementsIncludingExtensions()) {
if (providersToImplement.contains(element.getProvider())) {
if (!_networkModel.isProviderEnabledInPhysicalNetwork(_networkModel.getPhysicalNetworkId(network), element.getProvider().getName())) {
throw new CloudRuntimeException("Service provider " + element.getProvider().getName() + " either doesn't exist or is not enabled in physical network id: "
@@ -2446,7 +2476,7 @@ public void rollbackNicForMigration(final VirtualMachineProfile src, final Virtu
}
final List providersToImplement = getNetworkProviders(network.getId());
- for (final NetworkElement element : networkElements) {
+ for (final NetworkElement element : getNetworkElementsIncludingExtensions()) {
if (providersToImplement.contains(element.getProvider())) {
if (!_networkModel.isProviderEnabledInPhysicalNetwork(_networkModel.getPhysicalNetworkId(network), element.getProvider().getName())) {
throw new CloudRuntimeException("Service provider " + element.getProvider().getName() + " either doesn't exist or is not enabled in physical network id: "
@@ -2533,7 +2563,7 @@ public Pair doInTransaction(final TransactionStatus status)
final Network network = networkToRelease.first();
final NicProfile profile = networkToRelease.second();
final List providersToImplement = getNetworkProviders(network.getId());
- for (final NetworkElement element : networkElements) {
+ for (final NetworkElement element : getNetworkElementsIncludingExtensions()) {
if (providersToImplement.contains(element.getProvider())) {
logger.debug("Asking {} to release {}", element.getName(), profile);
//NOTE: Context appear to never be used in release method
@@ -2596,7 +2626,7 @@ protected void removeNic(final VirtualMachineProfile vm, final NicVO nic) {
*/
if (nic.getReservationStrategy() == Nic.ReservationStrategy.Create) {
final List providersToImplement = getNetworkProviders(network.getId());
- for (final NetworkElement element : networkElements) {
+ for (final NetworkElement element : getNetworkElementsIncludingExtensions()) {
if (providersToImplement.contains(element.getProvider())) {
logger.debug("Asking {} to release {}, according to the reservation strategy {}.", element.getName(), nic, nic.getReservationStrategy());
try {
@@ -3310,7 +3340,7 @@ public boolean shutdownNetworkElementsAndResources(final ReservationContext cont
// 2) Shutdown all the network elements
boolean success = true;
- for (final NetworkElement element : networkElements) {
+ for (final NetworkElement element : getNetworkElementsIncludingExtensions()) {
if (providersToShutdown.contains(element.getProvider())) {
try {
logger.debug("Sending network shutdown to {}", element.getName());
@@ -3421,7 +3451,7 @@ public boolean destroyNetwork(final long networkId, final ReservationContext con
// get providers to destroy
final List providersToDestroy = getNetworkProviders(network.getId());
- for (final NetworkElement element : networkElements) {
+ for (final NetworkElement element : getNetworkElementsIncludingExtensions()) {
if (providersToDestroy.contains(element.getProvider())) {
try {
logger.debug("Sending destroy to {}", element);
@@ -3792,7 +3822,7 @@ public boolean areRoutersRunning(final List extends VirtualRouter> routers) {
public void cleanupNicDhcpDnsEntry(Network network, VirtualMachineProfile vmProfile, NicProfile nicProfile) {
final List networkProviders = getNetworkProviders(network.getId());
- for (final NetworkElement element : networkElements) {
+ for (final NetworkElement element : getNetworkElementsIncludingExtensions()) {
if (networkProviders.contains(element.getProvider())) {
if (!_networkModel.isProviderEnabledInPhysicalNetwork(_networkModel.getPhysicalNetworkId(network), element.getProvider().getName())) {
throw new CloudRuntimeException("Service provider " + element.getProvider().getName() + " either doesn't exist or is not enabled in physical network id: "
@@ -3828,7 +3858,7 @@ public void cleanupNicDhcpDnsEntry(Network network, VirtualMachineProfile vmProf
* @throws InsufficientCapacityException
*/
private boolean rollingRestartRouters(final NetworkVO network, final NetworkOffering offering, final DeployDestination dest, final ReservationContext context) throws ResourceUnavailableException, ConcurrentOperationException, InsufficientCapacityException {
- if (!NetworkOrchestrationService.RollingRestartEnabled.value()) {
+ if (!isRollingRestartSupport(network)) {
if (shutdownNetworkElementsAndResources(context, true, network)) {
implementNetworkElementsAndResources(dest, context, network, offering);
return true;
@@ -3876,6 +3906,20 @@ private boolean rollingRestartRouters(final NetworkVO network, final NetworkOffe
return areRoutersRunning(routerDao.findByNetwork(network.getId()));
}
+ private boolean isRollingRestartSupport(final NetworkVO network) {
+ if (!NetworkOrchestrator.RollingRestartEnabled.value()) {
+ return false;
+ }
+ List services = _ntwkSrvcDao.getServicesInNetwork(network.getId());
+ for (NetworkServiceMapVO service : services) {
+ NetworkElement element = _networkModel.getElementImplementingProvider(service.getProvider());
+ if (element == null || !element.rollingRestartSupported()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
private void setRestartRequired(final NetworkVO network, final boolean restartRequired) {
logger.debug("Marking network {} with restartRequired={}", network, restartRequired);
network.setRestartRequired(restartRequired);
@@ -4437,6 +4481,12 @@ public Map finalizeServicesAndProvidersForNetwork(final NetworkO
if (provider == null) {
provider = _networkModel.getDefaultUniqueProviderForService(service).getName();
+ } else {
+ final Provider resolvedProvider = _networkModel.resolveProvider(provider);
+ if (resolvedProvider == null) {
+ throw new InvalidParameterValueException("Invalid provider " + provider + " configured for service " + service);
+ }
+ provider = resolvedProvider.getName();
}
// check that provider is supported
@@ -4462,7 +4512,10 @@ private List getNetworkProviders(final long networkId) {
final List providerNames = _ntwkSrvcDao.getDistinctProviders(networkId);
final List providers = new ArrayList<>();
for (final String providerName : providerNames) {
- providers.add(Network.Provider.getProvider(providerName));
+ final Provider provider = _networkModel.resolveProvider(providerName);
+ if (provider != null) {
+ providers.add(provider);
+ }
}
return providers;
@@ -4628,7 +4681,7 @@ private Map> getServiceProvidersMap(final long networkId)
if (providers == null) {
providers = new HashSet<>();
}
- providers.add(Provider.getProvider(nsm.getProvider()));
+ providers.add(_networkModel.resolveProvider(nsm.getProvider()));
map.put(Service.getService(nsm.getService()), providers);
}
return map;
@@ -4913,10 +4966,10 @@ public void unmanageNics(VirtualMachineProfile vm) {
@Override
public void expungeLbVmRefs(List vmIds, Long batchSize) {
- if (CollectionUtils.isEmpty(networkElements) || CollectionUtils.isEmpty(vmIds)) {
+ if (CollectionUtils.isEmpty(vmIds)) {
return;
}
- for (NetworkElement element : networkElements) {
+ for (NetworkElement element : getNetworkElementsIncludingExtensions()) {
if (element instanceof LoadBalancingServiceProvider) {
LoadBalancingServiceProvider lbProvider = (LoadBalancingServiceProvider)element;
lbProvider.expungeLbVmRefs(vmIds, batchSize);
diff --git a/engine/orchestration/src/test/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestratorTest.java b/engine/orchestration/src/test/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestratorTest.java
index e3989737112d..f03affa07281 100644
--- a/engine/orchestration/src/test/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestratorTest.java
+++ b/engine/orchestration/src/test/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestratorTest.java
@@ -27,6 +27,7 @@
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -35,11 +36,15 @@
import com.cloud.exception.InsufficientVirtualNetworkCapacityException;
import com.cloud.network.IpAddressManager;
import com.cloud.utils.Pair;
+import org.apache.cloudstack.extension.Extension;
+import org.apache.cloudstack.extension.ExtensionHelper;
+import org.apache.cloudstack.framework.extensions.network.NetworkExtensionElement;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
+import org.springframework.test.util.ReflectionTestUtils;
import org.mockito.ArgumentMatchers;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
@@ -68,6 +73,7 @@
import com.cloud.network.dao.PhysicalNetworkVO;
import com.cloud.network.dao.RouterNetworkDao;
import com.cloud.network.element.DhcpServiceProvider;
+import com.cloud.network.element.NetworkElement;
import com.cloud.network.guru.GuestNetworkGuru;
import com.cloud.network.guru.NetworkGuru;
import com.cloud.network.vpc.VpcManager;
@@ -105,6 +111,7 @@ public class NetworkOrchestratorTest extends TestCase {
private String guruName = "GuestNetworkGuru";
private String dhcpProvider = "VirtualRouter";
private NetworkGuru guru = mock(NetworkGuru.class);
+ private NetworkExtensionElement networkExtensionElement;
NetworkOfferingVO networkOffering = mock(NetworkOfferingVO.class);
@@ -135,6 +142,9 @@ public void setUp() {
testOrchestrator.routerJoinDao = mock(DomainRouterJoinDao.class);
testOrchestrator._ipAddrMgr = mock(IpAddressManager.class);
testOrchestrator._entityMgr = mock(EntityManager.class);
+ testOrchestrator.extensionHelper = mock(ExtensionHelper.class);
+ networkExtensionElement = mock(NetworkExtensionElement.class);
+ ReflectionTestUtils.setField(testOrchestrator, "networkExtensionElement", networkExtensionElement);
DhcpServiceProvider provider = mock(DhcpServiceProvider.class);
Map capabilities = new HashMap();
@@ -1010,4 +1020,63 @@ public void testImportNicWithIP4Address() throws Exception {
assertEquals("testtag", nicProfile.getName());
}
}
+
+ // -----------------------------------------------------------------------
+ // Tests for getNetworkElementsIncludingExtensions
+ // -----------------------------------------------------------------------
+
+ @Test
+ public void getNetworkElementsIncludingExtensionsReturnsBaseListWhenNoExtensions() {
+ when(testOrchestrator.extensionHelper.listExtensionsByType(Extension.Type.NetworkOrchestrator))
+ .thenReturn(Collections.emptyList());
+
+ DhcpServiceProvider dhcpProvider = mock(DhcpServiceProvider.class);
+ List elements = new ArrayList<>(List.of(dhcpProvider));
+ testOrchestrator.networkElements = elements;
+
+ @SuppressWarnings("unchecked")
+ List result =
+ (List) ReflectionTestUtils
+ .invokeMethod(testOrchestrator, "getNetworkElementsIncludingExtensions");
+ assertNotNull(result);
+ assertEquals(elements.size(), result.size());
+ }
+
+ @Test
+ public void getNetworkElementsIncludingExtensionsAddsExtensionElements() {
+ Extension ext = mock(Extension.class);
+ when(ext.getName()).thenReturn("my-net-ext");
+ when(testOrchestrator.extensionHelper.listExtensionsByType(Extension.Type.NetworkOrchestrator))
+ .thenReturn(List.of(ext));
+
+ NetworkExtensionElement extElement = mock(NetworkExtensionElement.class);
+ when(networkExtensionElement.withProviderName("my-net-ext")).thenReturn(extElement);
+
+ DhcpServiceProvider dhcpProvider = mock(DhcpServiceProvider.class);
+ testOrchestrator.networkElements = new ArrayList<>(List.of(dhcpProvider));
+
+ @SuppressWarnings("unchecked")
+ List result =
+ (List) ReflectionTestUtils
+ .invokeMethod(testOrchestrator, "getNetworkElementsIncludingExtensions");
+ assertNotNull(result);
+ assertEquals(2, result.size());
+ assertTrue(result.contains(extElement));
+ }
+
+ @Test
+ public void getNetworkElementsIncludingExtensionsReturnsBaseListWhenExtensionHelperReturnsNull() {
+ when(testOrchestrator.extensionHelper.listExtensionsByType(Extension.Type.NetworkOrchestrator))
+ .thenReturn(null);
+
+ DhcpServiceProvider dhcpProvider = mock(DhcpServiceProvider.class);
+ testOrchestrator.networkElements = new ArrayList<>(List.of(dhcpProvider));
+
+ @SuppressWarnings("unchecked")
+ List result =
+ (List) ReflectionTestUtils
+ .invokeMethod(testOrchestrator, "getNetworkElementsIncludingExtensions");
+ assertNotNull(result);
+ assertEquals(1, result.size());
+ }
}
diff --git a/engine/schema/src/main/java/com/cloud/network/dao/NetworkDaoImpl.java b/engine/schema/src/main/java/com/cloud/network/dao/NetworkDaoImpl.java
index 9f7ffabac930..e2dddeed13c4 100644
--- a/engine/schema/src/main/java/com/cloud/network/dao/NetworkDaoImpl.java
+++ b/engine/schema/src/main/java/com/cloud/network/dao/NetworkDaoImpl.java
@@ -37,7 +37,6 @@
import com.cloud.network.Network;
import com.cloud.network.Network.Event;
import com.cloud.network.Network.GuestType;
-import com.cloud.network.Network.Provider;
import com.cloud.network.Network.Service;
import com.cloud.network.Network.State;
import com.cloud.network.Networks.BroadcastDomainType;
@@ -390,7 +389,7 @@ public void persistNetworkServiceProviders(final long networkId, final Map services) {
this.setUserdataServiceProvided(services.contains(Service.UserData));
this.setSecuritygroupServiceProvided(services.contains(Service.SecurityGroup));
this.setNetworkAclServiceProvided(services.contains(Service.NetworkACL));
+ this.setCustomActionServiceProvided(services.contains(Service.CustomAction));
}
@Override
@@ -316,6 +320,9 @@ public List getEnabledServices() {
if (this.isSecuritygroupServiceProvided()) {
services.add(Service.SecurityGroup);
}
+ if (this.isCustomActionServiceProvided()) {
+ services.add(Service.CustomAction);
+ }
return services;
}
@@ -327,4 +334,12 @@ public boolean isNetworkAclServiceProvided() {
public void setNetworkAclServiceProvided(boolean networkAclServiceProvided) {
this.networkAclServiceProvided = networkAclServiceProvided;
}
+
+ public boolean isCustomActionServiceProvided() {
+ return customActionServiceProvided;
+ }
+
+ public void setCustomActionServiceProvided(boolean customActionServiceProvided) {
+ this.customActionServiceProvided = customActionServiceProvided;
+ }
}
diff --git a/engine/schema/src/main/java/com/cloud/network/vpc/VpcServiceMapVO.java b/engine/schema/src/main/java/com/cloud/network/vpc/VpcServiceMapVO.java
index 9fa4b505c650..ae156d7d4304 100644
--- a/engine/schema/src/main/java/com/cloud/network/vpc/VpcServiceMapVO.java
+++ b/engine/schema/src/main/java/com/cloud/network/vpc/VpcServiceMapVO.java
@@ -25,8 +25,6 @@
import javax.persistence.Id;
import javax.persistence.Table;
-import com.cloud.network.Network.Provider;
-import com.cloud.network.Network.Service;
import com.cloud.utils.db.GenericDao;
@Entity
@@ -72,10 +70,10 @@ public Date getCreated() {
public VpcServiceMapVO() {
}
- public VpcServiceMapVO(long vpcId, Service service, Provider provider) {
+ public VpcServiceMapVO(long vpcId, String serviceName, String providerName) {
this.vpcId = vpcId;
- this.service = service.getName();
- this.provider = provider.getName();
+ this.service = serviceName;
+ this.provider = providerName;
}
@Override
diff --git a/engine/schema/src/main/java/com/cloud/network/vpc/dao/VpcDaoImpl.java b/engine/schema/src/main/java/com/cloud/network/vpc/dao/VpcDaoImpl.java
index 4e13fe4f5d0d..cecdce1aba42 100644
--- a/engine/schema/src/main/java/com/cloud/network/vpc/dao/VpcDaoImpl.java
+++ b/engine/schema/src/main/java/com/cloud/network/vpc/dao/VpcDaoImpl.java
@@ -142,7 +142,7 @@ public void persistVpcServiceProviders(long vpcId, Map> ser
txn.start();
for (String service : serviceProviderMap.keySet()) {
for (String provider : serviceProviderMap.get(service)) {
- VpcServiceMapVO serviceMap = new VpcServiceMapVO(vpcId, Network.Service.getService(service), Network.Provider.getProvider(provider));
+ VpcServiceMapVO serviceMap = new VpcServiceMapVO(vpcId, Network.Service.getService(service).getName(), provider);
_vpcSvcMap.persist(serviceMap);
}
}
diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql
index 4cb9eb7cb2c4..1426b13e35ac 100644
--- a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql
+++ b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql
@@ -117,3 +117,10 @@ CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.vpc_offerings','conserve_mode', 'tin
--- Disable/enable NICs
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.nics','enabled', 'TINYINT(1) NOT NULL DEFAULT 1 COMMENT ''Indicates whether the NIC is enabled or not'' ');
+
+-- Increase length of value of extension details from 255 to 4096 to support longer details value
+CALL `cloud`.`IDEMPOTENT_CHANGE_COLUMN`('cloud.extension_details', 'value', 'value', 'VARCHAR(4096)');
+CALL `cloud`.`IDEMPOTENT_CHANGE_COLUMN`('cloud.extension_resource_map_details', 'value', 'value', 'VARCHAR(4096)');
+
+-- Add CustomAction service support to physical_network_service_providers
+CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.physical_network_service_providers', 'custom_action_service_provided', 'tinyint(1) unsigned NOT NULL DEFAULT 0 COMMENT "Is Custom Action service provided"');
diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/ListExtensionsCmd.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/ListExtensionsCmd.java
index 4426f259380b..08f375a3922f 100644
--- a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/ListExtensionsCmd.java
+++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/ListExtensionsCmd.java
@@ -70,6 +70,17 @@ public class ListExtensionsCmd extends BaseListCmd {
+ " When no parameters are passed, all the details are returned.")
private List details;
+ @Parameter(name = ApiConstants.TYPE, type = CommandType.STRING, description = "Type of the extension (e.g. Orchestrator, NetworkOrchestrator)")
+ private String type;
+
+ @Parameter(name = ApiConstants.RESOURCE_ID, type = CommandType.STRING,
+ description = "ID of the resource to list registered extensions for (e.g. cluster UUID, physical network UUID)")
+ private String resourceId;
+
+ @Parameter(name = ApiConstants.RESOURCE_TYPE, type = CommandType.STRING,
+ description = "Type of the resource (e.g. Cluster, PhysicalNetwork)")
+ private String resourceType;
+
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
@@ -82,6 +93,18 @@ public Long getExtensionId() {
return extensionId;
}
+ public String getType() {
+ return type;
+ }
+
+ public String getResourceId() {
+ return resourceId;
+ }
+
+ public String getResourceType() {
+ return resourceType;
+ }
+
public EnumSet getDetails() throws InvalidParameterValueException {
if (CollectionUtils.isEmpty(details)) {
return EnumSet.of(ApiConstants.ExtensionDetails.all);
diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/UpdateRegisteredExtensionCmd.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/UpdateRegisteredExtensionCmd.java
new file mode 100644
index 000000000000..6c755ecd1a54
--- /dev/null
+++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/UpdateRegisteredExtensionCmd.java
@@ -0,0 +1,116 @@
+// 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.
+
+package org.apache.cloudstack.framework.extensions.api;
+
+import java.util.EnumSet;
+import java.util.Map;
+
+import javax.inject.Inject;
+
+import org.apache.cloudstack.acl.RoleType;
+import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.ApiCommandResourceType;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.BaseCmd;
+import org.apache.cloudstack.api.Parameter;
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.response.ExtensionResponse;
+import org.apache.cloudstack.extension.Extension;
+import org.apache.cloudstack.framework.extensions.manager.ExtensionsManager;
+
+import com.cloud.user.Account;
+
+@APICommand(name = "updateRegisteredExtension",
+ description = "Update details for an extension registered with a resource",
+ responseObject = ExtensionResponse.class,
+ responseHasSensitiveInfo = false,
+ entityType = {Extension.class},
+ authorized = {RoleType.Admin},
+ since = "4.23.0")
+public class UpdateRegisteredExtensionCmd extends BaseCmd {
+
+ @Inject
+ ExtensionsManager extensionsManager;
+
+ @Parameter(name = ApiConstants.EXTENSION_ID, type = CommandType.UUID, required = true,
+ entityType = ExtensionResponse.class, description = "ID of the extension")
+ private Long extensionId;
+
+ @Parameter(name = ApiConstants.RESOURCE_ID, type = CommandType.STRING, required = true,
+ description = "ID of the resource where the extension is registered")
+ private String resourceId;
+
+ @Parameter(name = ApiConstants.RESOURCE_TYPE, type = CommandType.STRING, required = true,
+ description = "Type of the resource")
+ private String resourceType;
+
+ @Parameter(name = ApiConstants.DETAILS, type = CommandType.MAP,
+ description = "Details in key/value pairs using format details[i].keyname=keyvalue. Example: details[0].endpoint.url=urlvalue")
+ protected Map details;
+
+ @Parameter(name = ApiConstants.CLEAN_UP_DETAILS,
+ type = CommandType.BOOLEAN,
+ description = "Optional boolean field, which indicates if details should be cleaned up or not " +
+ "(If set to true, details removed for this registration, details field ignored; " +
+ "if false or not set, details can be updated through details map)")
+ private Boolean cleanupDetails;
+
+ public Long getExtensionId() {
+ return extensionId;
+ }
+
+ public String getResourceId() {
+ return resourceId;
+ }
+
+ public String getResourceType() {
+ return resourceType;
+ }
+
+ public Map getDetails() {
+ return convertDetailsToMap(details);
+ }
+
+ public Boolean isCleanupDetails() {
+ return cleanupDetails;
+ }
+
+ @Override
+ public void execute() throws ServerApiException {
+ Extension extension = extensionsManager.updateRegisteredExtensionWithResource(this);
+ ExtensionResponse response = extensionsManager.createExtensionResponse(extension,
+ EnumSet.of(ApiConstants.ExtensionDetails.all));
+ response.setResponseName(getCommandName());
+ setResponseObject(response);
+ }
+
+ @Override
+ public long getEntityOwnerId() {
+ return Account.ACCOUNT_ID_SYSTEM;
+ }
+
+ @Override
+ public ApiCommandResourceType getApiResourceType() {
+ return ApiCommandResourceType.Extension;
+ }
+
+ @Override
+ public Long getApiResourceId() {
+ return getExtensionId();
+ }
+}
diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionDao.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionDao.java
index 3355457ed25b..9bb47e4869b2 100644
--- a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionDao.java
+++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionDao.java
@@ -16,6 +16,9 @@
// under the License.
package org.apache.cloudstack.framework.extensions.dao;
+import java.util.List;
+
+import org.apache.cloudstack.extension.Extension;
import org.apache.cloudstack.framework.extensions.vo.ExtensionVO;
import com.cloud.utils.db.GenericDao;
@@ -23,4 +26,6 @@
public interface ExtensionDao extends GenericDao {
ExtensionVO findByName(String name);
+
+ List listByType(Extension.Type type);
}
diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionDaoImpl.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionDaoImpl.java
index 8e17199de6ca..ed215aa53d03 100644
--- a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionDaoImpl.java
+++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionDaoImpl.java
@@ -17,6 +17,9 @@
package org.apache.cloudstack.framework.extensions.dao;
+import java.util.List;
+
+import org.apache.cloudstack.extension.Extension;
import org.apache.cloudstack.framework.extensions.vo.ExtensionVO;
import com.cloud.utils.db.GenericDaoBase;
@@ -39,7 +42,13 @@ public ExtensionDaoImpl() {
public ExtensionVO findByName(String name) {
SearchCriteria sc = AllFieldSearch.create();
sc.setParameters("name", name);
-
return findOneBy(sc);
}
+
+ @Override
+ public List listByType(Extension.Type type) {
+ SearchCriteria sc = AllFieldSearch.create();
+ sc.setParameters("type", type);
+ return listBy(sc);
+ }
}
diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionResourceMapDao.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionResourceMapDao.java
index 930ef8675531..81da9ecb18fa 100644
--- a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionResourceMapDao.java
+++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionResourceMapDao.java
@@ -28,5 +28,9 @@ public interface ExtensionResourceMapDao extends GenericDao listResourceIdsByExtensionIdAndType(long extensionId,ExtensionResourceMap.ResourceType resourceType);
+ List listByResourceIdAndType(long resourceId, ExtensionResourceMap.ResourceType resourceType);
+
+ List listResourceIdsByExtensionIdAndType(long extensionId, ExtensionResourceMap.ResourceType resourceType);
+
+ List listResourceIdsByType(ExtensionResourceMap.ResourceType resourceType);
}
diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionResourceMapDaoImpl.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionResourceMapDaoImpl.java
index 6f19ef8b8b66..f81a10211207 100644
--- a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionResourceMapDaoImpl.java
+++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionResourceMapDaoImpl.java
@@ -55,6 +55,15 @@ public ExtensionResourceMapVO findByResourceIdAndType(long resourceId,
return findOneBy(sc);
}
+ @Override
+ public List listByResourceIdAndType(long resourceId,
+ ExtensionResourceMap.ResourceType resourceType) {
+ SearchCriteria sc = genericSearch.create();
+ sc.setParameters("resourceId", resourceId);
+ sc.setParameters("resourceType", resourceType);
+ return listBy(sc);
+ }
+
@Override
public List listResourceIdsByExtensionIdAndType(long extensionId, ExtensionResourceMap.ResourceType resourceType) {
GenericSearchBuilder sb = createSearchBuilder(Long.class);
@@ -67,4 +76,15 @@ public List listResourceIdsByExtensionIdAndType(long extensionId, Extensio
sc.setParameters("resourceType", resourceType);
return customSearch(sc, null);
}
+
+ @Override
+ public List listResourceIdsByType(ExtensionResourceMap.ResourceType resourceType) {
+ GenericSearchBuilder sb = createSearchBuilder(Long.class);
+ sb.selectFields(sb.entity().getResourceId());
+ sb.and("resourceType", sb.entity().getResourceType(), SearchCriteria.Op.EQ);
+ sb.done();
+ SearchCriteria sc = sb.create();
+ sc.setParameters("resourceType", resourceType);
+ return customSearch(sc, null);
+ }
}
diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManager.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManager.java
index 1b1a175c5975..cd033badff15 100644
--- a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManager.java
+++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManager.java
@@ -42,6 +42,7 @@
import org.apache.cloudstack.framework.extensions.api.UnregisterExtensionCmd;
import org.apache.cloudstack.framework.extensions.api.UpdateCustomActionCmd;
import org.apache.cloudstack.framework.extensions.api.UpdateExtensionCmd;
+import org.apache.cloudstack.framework.extensions.api.UpdateRegisteredExtensionCmd;
import org.apache.cloudstack.framework.extensions.command.ExtensionServerActionBaseCommand;
import com.cloud.agent.api.Answer;
@@ -65,6 +66,8 @@ public interface ExtensionsManager extends Manager {
Extension unregisterExtensionWithResource(UnregisterExtensionCmd cmd);
+ Extension updateRegisteredExtensionWithResource(UpdateRegisteredExtensionCmd cmd);
+
Extension updateExtension(UpdateExtensionCmd cmd);
Extension registerExtensionWithResource(RegisterExtensionCmd cmd);
diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManagerImpl.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManagerImpl.java
index 1422338ddc99..63509c1baeb0 100644
--- a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManagerImpl.java
+++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManagerImpl.java
@@ -26,6 +26,7 @@
import java.nio.file.Paths;
import java.security.InvalidParameterException;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
@@ -61,6 +62,7 @@
import org.apache.cloudstack.extension.Extension;
import org.apache.cloudstack.extension.ExtensionCustomAction;
import org.apache.cloudstack.extension.ExtensionHelper;
+import org.apache.cloudstack.extension.NetworkCustomActionProvider;
import org.apache.cloudstack.extension.ExtensionResourceMap;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.framework.config.Configurable;
@@ -75,6 +77,7 @@
import org.apache.cloudstack.framework.extensions.api.UnregisterExtensionCmd;
import org.apache.cloudstack.framework.extensions.api.UpdateCustomActionCmd;
import org.apache.cloudstack.framework.extensions.api.UpdateExtensionCmd;
+import org.apache.cloudstack.framework.extensions.api.UpdateRegisteredExtensionCmd;
import org.apache.cloudstack.framework.extensions.command.CleanupExtensionFilesCommand;
import org.apache.cloudstack.framework.extensions.command.ExtensionRoutingUpdateCommand;
import org.apache.cloudstack.framework.extensions.command.ExtensionServerActionBaseCommand;
@@ -125,6 +128,21 @@
import com.cloud.host.dao.HostDetailsDao;
import com.cloud.hypervisor.ExternalProvisioner;
import com.cloud.hypervisor.Hypervisor;
+import com.cloud.network.Network;
+import com.cloud.network.Network.Capability;
+import com.cloud.network.Network.Service;
+import com.cloud.network.NetworkModel;
+import com.cloud.network.PhysicalNetworkServiceProvider;
+import com.cloud.network.dao.NetworkDao;
+import com.cloud.network.dao.NetworkServiceMapDao;
+import com.cloud.network.dao.NetworkVO;
+import com.cloud.network.dao.PhysicalNetworkServiceProviderDao;
+import com.cloud.network.dao.PhysicalNetworkServiceProviderVO;
+import com.cloud.network.element.NetworkElement;
+import com.cloud.network.dao.PhysicalNetworkDao;
+import com.cloud.network.dao.PhysicalNetworkVO;
+import com.cloud.network.vpc.Vpc;
+import com.cloud.network.vpc.dao.VpcServiceMapDao;
import com.cloud.org.Cluster;
import com.cloud.serializer.GsonHelper;
import com.cloud.storage.dao.VMTemplateDao;
@@ -141,6 +159,10 @@
import com.cloud.utils.db.SearchCriteria;
import com.cloud.utils.db.Transaction;
import com.cloud.utils.db.TransactionCallbackWithException;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.vm.VirtualMachine;
import com.cloud.vm.VirtualMachineManager;
@@ -171,6 +193,12 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
@Inject
ClusterDao clusterDao;
+ @Inject
+ PhysicalNetworkDao physicalNetworkDao;
+
+ @Inject
+ PhysicalNetworkServiceProviderDao physicalNetworkServiceProviderDao;
+
@Inject
AgentManager agentMgr;
@@ -210,6 +238,18 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
@Inject
VMTemplateDao templateDao;
+ @Inject
+ NetworkDao networkDao;
+
+ @Inject
+ NetworkServiceMapDao networkServiceMapDao;
+
+ @Inject
+ VpcServiceMapDao vpcServiceMapDao;
+
+ @Inject
+ NetworkModel networkModel;
+
@Inject
RoleService roleService;
@@ -339,6 +379,39 @@ protected Pair getChecksumForExtensionPathOnMSPeer(Extension ex
return getResultFromAnswersString(answersStr, extension, msHost, "get path checksum");
}
+ protected List buildExtensionResourceDetailsArray(long extensionResourceMapId,
+ Map details) {
+ List detailsList = new ArrayList<>();
+ if (MapUtils.isEmpty(details)) {
+ return detailsList;
+ }
+ for (Map.Entry entry : details.entrySet()) {
+ boolean display = !SENSITIVE_DETAIL_KEYS.contains(entry.getKey().toLowerCase());
+ detailsList.add(new ExtensionResourceMapDetailsVO(extensionResourceMapId, entry.getKey(),
+ entry.getValue(), display));
+ }
+ return detailsList;
+ }
+
+ protected void appendHiddenExtensionResourceDetails(long extensionResourceMapId,
+ List detailsList) {
+ if (CollectionUtils.isEmpty(detailsList)) {
+ return;
+ }
+ Map hiddenDetails = extensionResourceMapDetailsDao.listDetailsKeyPairs(extensionResourceMapId, false);
+ if (MapUtils.isEmpty(hiddenDetails)) {
+ return;
+ }
+ Set requestedKeys = detailsList.stream()
+ .map(ExtensionResourceMapDetailsVO::getName)
+ .collect(Collectors.toSet());
+ hiddenDetails.forEach((key, value) -> {
+ if (!requestedKeys.contains(key)) {
+ detailsList.add(new ExtensionResourceMapDetailsVO(extensionResourceMapId, key, value, false));
+ }
+ });
+ }
+
protected List getParametersListFromMap(String actionName, Map parametersMap) {
if (MapUtils.isEmpty(parametersMap)) {
return Collections.emptyList();
@@ -370,16 +443,49 @@ protected Extension getExtensionFromResource(ExtensionCustomAction.ResourceType
VirtualMachine vm = (VirtualMachine) object;
Pair clusterHostId = virtualMachineManager.findClusterAndHostIdForVm(vm, false);
clusterId = clusterHostId.first();
- }
- if (clusterId == null) {
+ if (clusterId == null) {
+ return null;
+ }
+ ExtensionResourceMapVO mapVO =
+ extensionResourceMapDao.findByResourceIdAndType(clusterId, ExtensionResourceMap.ResourceType.Cluster);
+ if (mapVO == null) {
+ return null;
+ }
+ return extensionDao.findById(mapVO.getExtensionId());
+ } else if (resourceType == ExtensionCustomAction.ResourceType.Network) {
+ Network network = (Network) object;
+ Long physicalNetworkId = network.getPhysicalNetworkId();
+ if (physicalNetworkId == null) {
+ return null;
+ }
+ // Use provider-based lookup: match the network's service-map providers
+ // against extension names registered on the physical network.
+ // This correctly handles multiple different extensions on the same physical network.
+ List providers = networkServiceMapDao.getDistinctProviders(network.getId());
+ if (CollectionUtils.isNotEmpty(providers)) {
+ for (String providerName : providers) {
+ Extension ext = getExtensionForPhysicalNetworkAndProvider(physicalNetworkId, providerName);
+ if (ext != null) {
+ return ext;
+ }
+ }
+ }
return null;
- }
- ExtensionResourceMapVO mapVO =
- extensionResourceMapDao.findByResourceIdAndType(clusterId, ExtensionResourceMap.ResourceType.Cluster);
- if (mapVO == null) {
+ } else if (resourceType == ExtensionCustomAction.ResourceType.Vpc) {
+ Vpc vpc = (Vpc) object;
+ // Find extension via the VPC's tier networks
+ List tierNetworks = networkDao.listByVpc(vpc.getId());
+ if (CollectionUtils.isNotEmpty(tierNetworks)) {
+ for (NetworkVO tierNetwork : tierNetworks) {
+ Extension ext = getExtensionFromResource(ExtensionCustomAction.ResourceType.Network, tierNetwork.getUuid());
+ if (ext != null) {
+ return ext;
+ }
+ }
+ }
return null;
}
- return extensionDao.findById(mapVO.getExtensionId());
+ return null;
}
protected String getActionMessage(boolean success, ExtensionCustomAction action, Extension extension,
@@ -694,25 +800,68 @@ public List listExtensions(ListExtensionsCmd cmd) {
Long id = cmd.getExtensionId();
String name = cmd.getName();
String keyword = cmd.getKeyword();
+ String typeStr = cmd.getType();
+ String resourceIdStr = cmd.getResourceId();
+ String resourceTypeStr = cmd.getResourceType();
+
+ // If resourceId + resourceType are specified, return only extensions registered to that resource
+ if (StringUtils.isNotBlank(resourceIdStr) && StringUtils.isNotBlank(resourceTypeStr)) {
+ if (!EnumUtils.isValidEnum(ExtensionResourceMap.ResourceType.class, resourceTypeStr)) {
+ throw new InvalidParameterValueException("Invalid resourcetype: " + resourceTypeStr);
+ }
+ ExtensionResourceMap.ResourceType resType = ExtensionResourceMap.ResourceType.valueOf(resourceTypeStr);
+ // Resolve resourceId to a DB id
+ long resolvedResourceId;
+ if (ExtensionResourceMap.ResourceType.PhysicalNetwork.equals(resType)) {
+ PhysicalNetworkVO pn = physicalNetworkDao.findByUuid(resourceIdStr);
+ if (pn == null) {
+ try { pn = physicalNetworkDao.findById(Long.parseLong(resourceIdStr)); } catch (NumberFormatException ignored) {}
+ }
+ if (pn == null) throw new InvalidParameterValueException("Invalid physical network ID: " + resourceIdStr);
+ resolvedResourceId = pn.getId();
+ } else {
+ try { resolvedResourceId = Long.parseLong(resourceIdStr); } catch (NumberFormatException e) {
+ throw new InvalidParameterValueException("Invalid resource ID: " + resourceIdStr);
+ }
+ }
+ List maps = extensionResourceMapDao.listByResourceIdAndType(resolvedResourceId, resType);
+ List responses = new ArrayList<>();
+ for (ExtensionResourceMapVO map : maps) {
+ ExtensionVO ext = extensionDao.findById(map.getExtensionId());
+ if (ext == null) continue;
+ if (typeStr != null && !typeStr.equalsIgnoreCase(ext.getType().name())) continue;
+ if (name != null && !name.equalsIgnoreCase(ext.getName())) continue;
+ responses.add(createExtensionResponse(ext, cmd.getDetails()));
+ }
+ return responses;
+ }
+
final SearchBuilder sb = extensionDao.createSearchBuilder();
final Filter searchFilter = new Filter(ExtensionVO.class, "id", false, cmd.getStartIndex(), cmd.getPageSizeVal());
sb.and("id", sb.entity().getId(), SearchCriteria.Op.EQ);
sb.and("name", sb.entity().getName(), SearchCriteria.Op.EQ);
sb.and("keyword", sb.entity().getName(), SearchCriteria.Op.LIKE);
+ sb.and("type", sb.entity().getType(), SearchCriteria.Op.EQ);
+ sb.done();
final SearchCriteria sc = sb.create();
if (id != null) {
sc.setParameters("id", id);
}
-
if (name != null) {
sc.setParameters("name", name);
}
-
if (keyword != null) {
sc.setParameters("keyword", "%" + keyword + "%");
}
+ if (typeStr != null) {
+ Extension.Type type = EnumUtils.getEnum(Extension.Type.class, typeStr);
+ if (type == null) {
+ throw new InvalidParameterValueException("Invalid type: " + typeStr);
+ }
+ sc.setParameters("type", type);
+ }
final Pair, Integer> result = extensionDao.searchAndCount(sc, searchFilter);
List responses = new ArrayList<>();
@@ -880,19 +1029,102 @@ public Extension registerExtensionWithResource(RegisterExtensionCmd cmd) {
String resourceType = cmd.getResourceType();
if (!EnumUtils.isValidEnum(ExtensionResourceMap.ResourceType.class, resourceType)) {
throw new InvalidParameterValueException(
- String.format("Currently only [%s] can be used to register an extension of type Orchestrator",
+ String.format("Currently only [%s] can be used to register an extension",
EnumSet.allOf(ExtensionResourceMap.ResourceType.class)));
}
+ ExtensionVO extension = extensionDao.findById(extensionId);
+ if (extension == null) {
+ throw new InvalidParameterValueException("Invalid extension specified");
+ }
+ ExtensionResourceMap.ResourceType resType = ExtensionResourceMap.ResourceType.valueOf(resourceType);
+ if (ExtensionResourceMap.ResourceType.PhysicalNetwork.equals(resType)) {
+ PhysicalNetworkVO physicalNetwork = physicalNetworkDao.findByUuid(resourceId);
+ if (physicalNetwork == null) {
+ physicalNetwork = physicalNetworkDao.findById(Long.parseLong(resourceId));
+ }
+ if (physicalNetwork == null) {
+ throw new InvalidParameterValueException("Invalid physical network ID specified");
+ }
+ ExtensionResourceMap extensionResourceMap = registerExtensionWithPhysicalNetwork(physicalNetwork, extension, cmd.getDetails());
+ return extensionDao.findById(extensionResourceMap.getExtensionId());
+ }
ClusterVO clusterVO = clusterDao.findByUuid(resourceId);
if (clusterVO == null) {
throw new InvalidParameterValueException("Invalid cluster ID specified");
}
+ ExtensionResourceMap extensionResourceMap = registerExtensionWithCluster(clusterVO, extension, cmd.getDetails());
+ return extensionDao.findById(extensionResourceMap.getExtensionId());
+ }
+
+ @Override
+ @ActionEvent(eventType = EventTypes.EVENT_EXTENSION_RESOURCE_UPDATE, eventDescription = "updating extension resource")
+ public Extension updateRegisteredExtensionWithResource(UpdateRegisteredExtensionCmd cmd) {
+ final String resourceId = cmd.getResourceId();
+ final Long extensionId = cmd.getExtensionId();
+ final String resourceType = cmd.getResourceType();
+ final Map details = cmd.getDetails();
+ final Boolean cleanupDetails = cmd.isCleanupDetails();
+
+ if (!EnumUtils.isValidEnum(ExtensionResourceMap.ResourceType.class, resourceType)) {
+ throw new InvalidParameterValueException(
+ String.format("Currently only [%s] can be used to update an extension registration",
+ EnumSet.allOf(ExtensionResourceMap.ResourceType.class)));
+ }
ExtensionVO extension = extensionDao.findById(extensionId);
if (extension == null) {
throw new InvalidParameterValueException("Invalid extension specified");
}
- ExtensionResourceMap extensionResourceMap = registerExtensionWithCluster(clusterVO, extension, cmd.getDetails());
- return extensionDao.findById(extensionResourceMap.getExtensionId());
+
+ ExtensionResourceMap.ResourceType resType = ExtensionResourceMap.ResourceType.valueOf(resourceType);
+ long resolvedResourceId;
+ if (ExtensionResourceMap.ResourceType.PhysicalNetwork.equals(resType)) {
+ PhysicalNetworkVO physicalNetwork = physicalNetworkDao.findByUuid(resourceId);
+ if (physicalNetwork == null) {
+ try {
+ physicalNetwork = physicalNetworkDao.findById(Long.parseLong(resourceId));
+ } catch (NumberFormatException ignored) {
+ }
+ }
+ if (physicalNetwork == null) {
+ throw new InvalidParameterValueException("Invalid physical network ID specified");
+ }
+ resolvedResourceId = physicalNetwork.getId();
+ } else {
+ ClusterVO clusterVO = clusterDao.findByUuid(resourceId);
+ if (clusterVO == null) {
+ throw new InvalidParameterValueException("Invalid cluster ID specified");
+ }
+ resolvedResourceId = clusterVO.getId();
+ }
+
+ List mappings = extensionResourceMapDao.listByResourceIdAndType(resolvedResourceId, resType);
+ ExtensionResourceMapVO targetMapping = null;
+ if (CollectionUtils.isNotEmpty(mappings)) {
+ for (ExtensionResourceMapVO mapping : mappings) {
+ if (mapping.getExtensionId() == extensionId) {
+ targetMapping = mapping;
+ break;
+ }
+ }
+ }
+ if (targetMapping == null) {
+ throw new InvalidParameterValueException(String.format(
+ "Extension '%s' is not registered with resource %s (%s)",
+ extension.getName(), resourceId, resourceType));
+ }
+
+ if (Boolean.TRUE.equals(cleanupDetails)) {
+ extensionResourceMapDetailsDao.removeDetails(targetMapping.getId());
+ } else if (MapUtils.isNotEmpty(details)) {
+ List detailsList = buildExtensionResourceDetailsArray(targetMapping.getId(), details);
+ appendHiddenExtensionResourceDetails(targetMapping.getId(), detailsList);
+ detailsList = detailsList.stream()
+ .filter(detail -> detail.getValue() != null)
+ .collect(Collectors.toList());
+ extensionResourceMapDetailsDao.saveDetails(detailsList);
+ }
+
+ return extensionDao.findById(extensionId);
}
@Override
@@ -923,8 +1155,9 @@ public ExtensionResourceMap registerExtensionWithCluster(Cluster cluster, Extens
List detailsVOList = new ArrayList<>();
if (MapUtils.isNotEmpty(details)) {
for (Map.Entry entry : details.entrySet()) {
+ boolean display = !SENSITIVE_DETAIL_KEYS.contains(entry.getKey().toLowerCase());
detailsVOList.add(new ExtensionResourceMapDetailsVO(savedExtensionMap.getId(),
- entry.getKey(), entry.getValue()));
+ entry.getKey(), entry.getValue(), display));
}
extensionResourceMapDetailsDao.saveDetails(detailsVOList);
}
@@ -934,6 +1167,246 @@ public ExtensionResourceMap registerExtensionWithCluster(Cluster cluster, Extens
return result;
}
+ protected ExtensionResourceMap registerExtensionWithPhysicalNetwork(PhysicalNetworkVO physicalNetwork,
+ Extension extension, Map details) {
+ // Only NetworkOrchestrator extensions can be registered with physical networks
+ if (!Extension.Type.NetworkOrchestrator.equals(extension.getType())) {
+ throw new InvalidParameterValueException(String.format(
+ "Only extensions of type %s can be registered with a physical network. "
+ + "Extension '%s' is of type %s.",
+ Extension.Type.NetworkOrchestrator.name(),
+ extension.getName(), extension.getType().name()));
+ }
+
+ // Block registering the exact same extension twice on the same physical network
+ final ExtensionResourceMap.ResourceType resourceType = ExtensionResourceMap.ResourceType.PhysicalNetwork;
+ List existing = extensionResourceMapDao.listByResourceIdAndType(
+ physicalNetwork.getId(), resourceType);
+ if (existing != null) {
+ for (ExtensionResourceMapVO ex : existing) {
+ if (ex.getExtensionId() == extension.getId()) {
+ throw new CloudRuntimeException(String.format(
+ "Extension '%s' is already registered with physical network %s",
+ extension.getName(), physicalNetwork.getId()));
+ }
+ }
+ }
+
+ // Resolve which services this extension provides from its network.services detail
+ Set services = resolveExtensionServices(extension);
+
+ return Transaction.execute((TransactionCallbackWithException) status -> {
+ // 1. Persist the extension<->physical-network mapping
+ ExtensionResourceMapVO extensionMap = new ExtensionResourceMapVO(extension.getId(),
+ physicalNetwork.getId(), resourceType);
+ ExtensionResourceMapVO savedExtensionMap = extensionResourceMapDao.persist(extensionMap);
+
+ // 2. Persist device credentials / details
+ List detailsVOList = new ArrayList<>();
+ if (MapUtils.isNotEmpty(details)) {
+ for (Map.Entry entry : details.entrySet()) {
+ boolean display = !SENSITIVE_DETAIL_KEYS.contains(entry.getKey().toLowerCase());
+ detailsVOList.add(new ExtensionResourceMapDetailsVO(savedExtensionMap.getId(),
+ entry.getKey(), entry.getValue(), display));
+ }
+ extensionResourceMapDetailsDao.saveDetails(detailsVOList);
+ }
+
+ // 3. Auto-create the NetworkServiceProvider entry for this extension so that
+ // the services are visible in the UI and in listSupportedNetworkServices.
+ // The NSP name equals the extension name; state is Enabled by default.
+ PhysicalNetworkServiceProviderVO existingNsp =
+ physicalNetworkServiceProviderDao.findByServiceProvider(
+ physicalNetwork.getId(), extension.getName());
+ if (existingNsp == null) {
+ PhysicalNetworkServiceProviderVO nsp =
+ new PhysicalNetworkServiceProviderVO(physicalNetwork.getId(), extension.getName());
+ applyServicesToNsp(nsp, services);
+ nsp.setState(PhysicalNetworkServiceProvider.State.Enabled);
+ physicalNetworkServiceProviderDao.persist(nsp);
+ logger.info("Auto-created NetworkServiceProvider '{}' (Enabled) for physical network {} "
+ + "with services {}", extension.getName(), physicalNetwork.getId(), services);
+ }
+
+ return savedExtensionMap;
+ });
+ }
+
+ /**
+ * Resolves the set of network service names declared in the extension's
+ * {@code network.services} detail. Falls back to an empty set if not present
+ */
+ private Set resolveExtensionServices(Extension extension) {
+ Map extDetails = extensionDetailsDao.listDetailsKeyPairs(extension.getId());
+ Set parsed = parseNetworkServicesFromDetailKeys(extDetails);
+ if (!parsed.isEmpty()) {
+ return parsed;
+ }
+ // Falls back to an empty set if not present
+ return new HashSet<>();
+ }
+
+ /**
+ * Resolves the set of service names from the extension detail map.
+ * From {@code network.services} comma-separated key.
+ */
+ private Set parseNetworkServicesFromDetailKeys(Map extDetails) {
+ if (extDetails == null) {
+ return Collections.emptySet();
+ }
+ // New format: "network.services" = "SourceNat,StaticNat,..."
+ if (extDetails.containsKey(ExtensionHelper.NETWORK_SERVICES_DETAIL_KEY)) {
+ String value = extDetails.get(ExtensionHelper.NETWORK_SERVICES_DETAIL_KEY);
+ if (StringUtils.isNotBlank(value)) {
+ Set services = new HashSet<>();
+ for (String s : value.split(",")) {
+ String trimmed = s.trim();
+ if (!trimmed.isEmpty()) {
+ services.add(trimmed);
+ }
+ }
+ if (!services.isEmpty()) {
+ return services;
+ }
+ }
+ }
+
+ return Collections.emptySet();
+ }
+
+ /**
+ * Builds a full {@code Map>} from the
+ * extension detail map. From the split keys
+ * {@code network.services} + {@code network.service.capabilities}.
+ */
+ private Map> buildCapabilitiesFromDetailKeys(
+ Map extDetails) {
+ if (extDetails == null) {
+ return new HashMap<>();
+ }
+ // New split format
+ if (extDetails.containsKey(ExtensionHelper.NETWORK_SERVICES_DETAIL_KEY)) {
+ Set serviceNames = parseNetworkServicesFromDetailKeys(extDetails);
+ if (!serviceNames.isEmpty()) {
+ JsonObject capsObj = null;
+ if (extDetails.containsKey(ExtensionHelper.NETWORK_SERVICE_CAPABILITIES_DETAIL_KEY)) {
+ try {
+ capsObj = JsonParser.parseString(
+ extDetails.get(ExtensionHelper.NETWORK_SERVICE_CAPABILITIES_DETAIL_KEY))
+ .getAsJsonObject();
+ } catch (Exception e) {
+ logger.warn("Failed to parse network.service.capabilities JSON: {}", e.getMessage());
+ }
+ }
+ Map> result = new HashMap<>();
+ for (String svcName : serviceNames) {
+ Service service = Service.getService(svcName);
+ if (service == null) {
+ logger.warn("Unknown network service '{}' in network.services — skipping", svcName);
+ continue;
+ }
+ Map capMap = new HashMap<>();
+ if (capsObj != null && capsObj.has(svcName)) {
+ JsonObject svcCaps = capsObj.getAsJsonObject(svcName);
+ for (Map.Entry entry : svcCaps.entrySet()) {
+ Capability cap = Capability.getCapability(entry.getKey());
+ if (cap != null) {
+ capMap.put(cap, entry.getValue().getAsString());
+ }
+ }
+ }
+ result.put(service, capMap);
+ }
+ return result;
+ }
+ }
+
+ return new HashMap<>();
+ }
+
+ /**
+ * Sets the boolean service-provided flags on a {@link PhysicalNetworkServiceProviderVO}
+ * based on a set of service names.
+ */
+ private void applyServicesToNsp(PhysicalNetworkServiceProviderVO nsp, Set services) {
+ nsp.setSourcenatServiceProvided(services.contains("SourceNat"));
+ nsp.setStaticnatServiceProvided(services.contains("StaticNat"));
+ nsp.setPortForwardingServiceProvided(services.contains("PortForwarding"));
+ nsp.setFirewallServiceProvided(services.contains("Firewall"));
+ nsp.setGatewayServiceProvided(services.contains("Gateway"));
+ nsp.setDnsServiceProvided(services.contains("Dns"));
+ nsp.setDhcpServiceProvided(services.contains("Dhcp"));
+ nsp.setUserdataServiceProvided(services.contains("UserData"));
+ nsp.setLbServiceProvided(services.contains("Lb"));
+ nsp.setVpnServiceProvided(services.contains("Vpn"));
+ nsp.setSecuritygroupServiceProvided(services.contains("SecurityGroup"));
+ nsp.setNetworkAclServiceProvided(services.contains("NetworkACL"));
+ nsp.setCustomActionServiceProvided(services.contains("CustomAction"));
+ }
+
+ /** Keys that are always stored with display=false (sensitive). */
+ private static final Set SENSITIVE_DETAIL_KEYS =
+ Set.of("password", "sshkey");
+
+ /**
+ * Validates that the comma-separated or JSON-array {@code servicesValue} is a
+ * subset of the services declared in the extension's {@code network.services}
+ * Throws {@link InvalidParameterValueException} if any service in the request is not
+ * offered by the extension.
+ */
+ protected void validateNetworkServicesSubset(Extension extension, String servicesValue) {
+ if (StringUtils.isBlank(servicesValue)) {
+ return;
+ }
+ Map extDetails = extensionDetailsDao.listDetailsKeyPairs(extension.getId());
+ Set allowedServices = parseNetworkServicesFromDetailKeys(extDetails);
+ if (allowedServices.isEmpty()) {
+ // No services declared → accept any
+ return;
+ }
+
+ // Parse the requested services: either comma-separated string or JSON array
+ List requested = parseServicesList(servicesValue);
+ List invalid = requested.stream()
+ .filter(s -> !allowedServices.contains(s))
+ .collect(Collectors.toList());
+ if (!invalid.isEmpty()) {
+ throw new InvalidParameterValueException(String.format(
+ "The following services are not supported by extension '%s': %s. "
+ + "Supported services are: %s",
+ extension.getName(), invalid, allowedServices));
+ }
+ }
+
+ /**
+ * Parses a services list from either a comma-separated string (e.g.
+ * {@code "SourceNat,StaticNat"}) or a JSON array (e.g.
+ * {@code ["SourceNat","StaticNat"]}).
+ */
+ private List parseServicesList(String value) {
+ if (StringUtils.isBlank(value)) {
+ return Collections.emptyList();
+ }
+ value = value.trim();
+ if (value.startsWith("[")) {
+ try {
+ JsonArray arr = JsonParser.parseString(value).getAsJsonArray();
+ List result = new ArrayList<>();
+ for (JsonElement el : arr) {
+ result.add(el.getAsString().trim());
+ }
+ return result;
+ } catch (Exception e) {
+ // fall through to comma-split
+ }
+ }
+ // Comma-separated
+ return Arrays.stream(value.split(","))
+ .map(String::trim)
+ .filter(s -> !s.isEmpty())
+ .collect(Collectors.toList());
+ }
+
@Override
@ActionEvent(eventType = EventTypes.EVENT_EXTENSION_RESOURCE_UNREGISTER, eventDescription = "unregistering extension resource")
public Extension unregisterExtensionWithResource(UnregisterExtensionCmd cmd) {
@@ -942,10 +1415,15 @@ public Extension unregisterExtensionWithResource(UnregisterExtensionCmd cmd) {
final String resourceType = cmd.getResourceType();
if (!EnumUtils.isValidEnum(ExtensionResourceMap.ResourceType.class, resourceType)) {
throw new InvalidParameterValueException(
- String.format("Currently only [%s] can be used to unregister an extension of type Orchestrator",
+ String.format("Currently only [%s] can be used to unregister an extension",
EnumSet.allOf(ExtensionResourceMap.ResourceType.class)));
}
- unregisterExtensionWithCluster(resourceId, extensionId);
+ ExtensionResourceMap.ResourceType resType = ExtensionResourceMap.ResourceType.valueOf(resourceType);
+ if (ExtensionResourceMap.ResourceType.PhysicalNetwork.equals(resType)) {
+ unregisterExtensionWithPhysicalNetwork(resourceId, extensionId);
+ } else {
+ unregisterExtensionWithCluster(resourceId, extensionId);
+ }
return extensionDao.findById(extensionId);
}
@@ -965,6 +1443,55 @@ public void unregisterExtensionWithCluster(Cluster cluster, Long extensionId) {
}
}
+ protected void unregisterExtensionWithPhysicalNetwork(String resourceId, Long extensionId) {
+ PhysicalNetworkVO physicalNetwork = physicalNetworkDao.findByUuid(resourceId);
+ if (physicalNetwork == null) {
+ try {
+ physicalNetwork = physicalNetworkDao.findById(Long.parseLong(resourceId));
+ } catch (NumberFormatException ignored) {
+ }
+ }
+ if (physicalNetwork == null) {
+ throw new InvalidParameterValueException("Invalid physical network ID specified");
+ }
+ // Find the specific map entry for this extension+physical-network combination
+ List existingList = extensionResourceMapDao.listByResourceIdAndType(
+ physicalNetwork.getId(), ExtensionResourceMap.ResourceType.PhysicalNetwork);
+ if (existingList == null || existingList.isEmpty()) {
+ return;
+ }
+ final long physNetId = physicalNetwork.getId();
+ for (ExtensionResourceMapVO existing : existingList) {
+ if (extensionId == null || existing.getExtensionId() == extensionId) {
+ ExtensionVO ext = extensionDao.findById(existing.getExtensionId());
+ if (ext != null) {
+ List networksUsingProvider = networkDao.listByPhysicalNetworkAndProvider(
+ physNetId, ext.getName());
+ if (CollectionUtils.isNotEmpty(networksUsingProvider)) {
+ throw new CloudRuntimeException(String.format(
+ "Cannot unregister extension '%s' from physical network %s. "
+ + "Provider is used by %d existing network service(s)",
+ ext.getName(), physNetId, networksUsingProvider.size()));
+ }
+ }
+
+ extensionResourceMapDao.remove(existing.getId());
+ extensionResourceMapDetailsDao.removeDetails(existing.getId());
+
+ // Also remove the auto-created NSP for this extension
+ if (ext != null) {
+ PhysicalNetworkServiceProviderVO nsp =
+ physicalNetworkServiceProviderDao.findByServiceProvider(physNetId, ext.getName());
+ if (nsp != null) {
+ physicalNetworkServiceProviderDao.remove(nsp.getId());
+ logger.info("Removed NetworkServiceProvider '{}' from physical network {} "
+ + "on extension unregister", ext.getName(), physNetId);
+ }
+ }
+ }
+ }
+ }
+
@Override
public ExtensionResponse createExtensionResponse(Extension extension,
EnumSet viewDetails) {
@@ -988,6 +1515,12 @@ public ExtensionResponse createExtensionResponse(Extension extension,
Cluster cluster = clusterDao.findById(extensionResourceMapVO.getResourceId());
extensionResourceResponse.setId(cluster.getUuid());
extensionResourceResponse.setName(cluster.getName());
+ } else if (ExtensionResourceMap.ResourceType.PhysicalNetwork.equals(extensionResourceMapVO.getResourceType())) {
+ PhysicalNetworkVO pn = physicalNetworkDao.findById(extensionResourceMapVO.getResourceId());
+ if (pn != null) {
+ extensionResourceResponse.setId(pn.getUuid());
+ extensionResourceResponse.setName(pn.getName());
+ }
}
Map details = extensionResourceMapDetailsDao.listDetailsKeyPairs(
extensionResourceMapVO.getId(), true);
@@ -1423,6 +1956,14 @@ public CustomActionResultResponse runCustomAction(RunCustomActionCmd cmd) {
Pair clusterAndHostId = virtualMachineManager.findClusterAndHostIdForVm(virtualMachine, false);
clusterId = clusterAndHostId.first();
hostId = clusterAndHostId.second();
+ } else if (entity instanceof Network) {
+ // Network custom action: dispatched directly to NetworkCustomActionProvider (no agent)
+ Network network = (Network) entity;
+ return runNetworkCustomAction(network, customActionVO, extensionVO, actionResourceType, cmdParameters);
+ } else if (entity instanceof Vpc) {
+ // VPC custom action: find a tier network and dispatch to the same NetworkCustomActionProvider
+ Vpc vpc = (Vpc) entity;
+ return runVpcCustomAction(vpc, customActionVO, extensionVO, actionResourceType, cmdParameters);
}
if (clusterId == null || hostId == null) {
@@ -1499,6 +2040,177 @@ public CustomActionResultResponse runCustomAction(RunCustomActionCmd cmd) {
return response;
}
+ /**
+ * Executes a custom action for a Network resource by delegating to an
+ * available {@link NetworkCustomActionProvider} (e.g. NetworkExtensionElement).
+ * This path does NOT go through the agent framework.
+ */
+ protected CustomActionResultResponse runNetworkCustomAction(Network network,
+ ExtensionCustomActionVO customActionVO, ExtensionVO extensionVO,
+ ExtensionCustomAction.ResourceType actionResourceType,
+ Map cmdParameters) {
+
+ final String actionName = customActionVO.getName();
+ CustomActionResultResponse response = new CustomActionResultResponse();
+ response.setId(customActionVO.getUuid());
+ response.setName(actionName);
+ response.setObjectName("customactionresult");
+ Map result = new HashMap<>();
+ response.setSuccess(false);
+ result.put(ApiConstants.MESSAGE, getActionMessage(false, customActionVO, extensionVO, actionResourceType, network));
+
+ // Resolve action parameters
+ List actionParameters = null;
+ Pair