diff --git a/api/src/main/java/com/cloud/network/Network.java b/api/src/main/java/com/cloud/network/Network.java
index be8a45cbccb..2f0bcdd5ef9 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 @@ public interface Network extends ControlledEntity, StateObject, I
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;
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 605cc8b6a79..245faa762a2 100644
--- a/api/src/main/java/org/apache/cloudstack/extension/ExtensionCustomAction.java
+++ b/api/src/main/java/org/apache/cloudstack/extension/ExtensionCustomAction.java
@@ -49,7 +49,8 @@ import com.google.gson.reflect.TypeToken;
public interface ExtensionCustomAction extends InternalIdentity, Identity {
enum ResourceType {
VirtualMachine(com.cloud.vm.VirtualMachine.class),
- Network(com.cloud.network.Network.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/NetworkCustomActionProvider.java b/api/src/main/java/org/apache/cloudstack/extension/NetworkCustomActionProvider.java
index 3f8754a168b..1c281c15f28 100644
--- a/api/src/main/java/org/apache/cloudstack/extension/NetworkCustomActionProvider.java
+++ b/api/src/main/java/org/apache/cloudstack/extension/NetworkCustomActionProvider.java
@@ -20,13 +20,15 @@ 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 (e.g. NetworkExtensionElement).
+ * 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}.
+ * {@code runCustomAction} requests whose resource type is {@code Network}
+ * or {@code Vpc}.
*/
public interface NetworkCustomActionProvider {
@@ -39,6 +41,15 @@ public interface NetworkCustomActionProvider {
*/
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.
@@ -49,4 +60,15 @@ public interface NetworkCustomActionProvider {
* @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/engine/schema/src/main/java/com/cloud/network/dao/PhysicalNetworkServiceProviderVO.java b/engine/schema/src/main/java/com/cloud/network/dao/PhysicalNetworkServiceProviderVO.java
index 9557c7465bf..217ee8ebc66 100644
--- a/engine/schema/src/main/java/com/cloud/network/dao/PhysicalNetworkServiceProviderVO.java
+++ b/engine/schema/src/main/java/com/cloud/network/dao/PhysicalNetworkServiceProviderVO.java
@@ -97,6 +97,9 @@ public class PhysicalNetworkServiceProviderVO implements PhysicalNetworkServiceP
@Column(name = "networkacl_service_provided")
boolean networkAclServiceProvided;
+ @Column(name = "custom_action_service_provided")
+ boolean customActionServiceProvided;
+
@Column(name = GenericDao.REMOVED_COLUMN)
Date removed;
@@ -278,6 +281,7 @@ public class PhysicalNetworkServiceProviderVO implements PhysicalNetworkServiceP
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 class PhysicalNetworkServiceProviderVO implements PhysicalNetworkServiceP
if (this.isSecuritygroupServiceProvided()) {
services.add(Service.SecurityGroup);
}
+ if (this.isCustomActionServiceProvided()) {
+ services.add(Service.CustomAction);
+ }
return services;
}
@@ -327,4 +334,12 @@ public class PhysicalNetworkServiceProviderVO implements PhysicalNetworkServiceP
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/resources/META-INF/db/schema-42210to42300.sql b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql
index 81a86f68d49..b3f56cf5d69 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
@@ -135,3 +135,6 @@ CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.vpc', 'keep_mac_address_on_public_ni
-- 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 1c9efdaca78..08f375a3922 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,7 +70,7 @@ 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). Default is Orchestrator if not set")
+ @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,
@@ -78,7 +78,7 @@ public class ListExtensionsCmd extends BaseListCmd {
private String resourceId;
@Parameter(name = ApiConstants.RESOURCE_TYPE, type = CommandType.STRING,
- description = "Type of the resource (e.g. Cluster, PhysicalNetwork). Default is Cluster if not set")
+ description = "Type of the resource (e.g. Cluster, PhysicalNetwork)")
private String resourceType;
/////////////////////////////////////////////////////
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 734c98956fa..70b5e07d414 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
@@ -141,6 +141,8 @@ 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;
@@ -242,6 +244,9 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
@Inject
NetworkServiceMapDao networkServiceMapDao;
+ @Inject
+ VpcServiceMapDao vpcServiceMapDao;
+
@Inject
NetworkModel networkModel;
@@ -472,6 +477,19 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
return null;
}
return extensionDao.findById(maps.get(0).getExtensionId());
+ } else if (resourceType == ExtensionCustomAction.ResourceType.Vpc) {
+ com.cloud.network.vpc.Vpc vpc = (com.cloud.network.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 null;
}
@@ -1103,19 +1121,13 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
if (Boolean.TRUE.equals(cleanupDetails)) {
extensionResourceMapDetailsDao.removeDetails(targetMapping.getId());
- } else {
+ } else if (MapUtils.isNotEmpty(details)) {
List detailsList = buildExtensionResourceDetailsArray(targetMapping.getId(), details);
- if (CollectionUtils.isNotEmpty(detailsList)) {
- appendHiddenExtensionResourceDetails(targetMapping.getId(), detailsList);
- }
+ appendHiddenExtensionResourceDetails(targetMapping.getId(), detailsList);
detailsList = detailsList.stream()
.filter(detail -> detail.getValue() != null)
.collect(Collectors.toList());
- if (CollectionUtils.isNotEmpty(detailsList)) {
- extensionResourceMapDetailsDao.saveDetails(detailsList);
- } else {
- extensionResourceMapDetailsDao.removeDetails(targetMapping.getId());
- }
+ extensionResourceMapDetailsDao.saveDetails(detailsList);
}
return extensionDao.findById(extensionId);
@@ -1222,7 +1234,7 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
+ "with services {}", extension.getName(), physicalNetwork.getId(), services);
}
- return extensionMap;
+ return savedExtensionMap;
});
}
@@ -1335,6 +1347,7 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
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). */
@@ -1463,7 +1476,7 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
if (CollectionUtils.isNotEmpty(networksUsingProvider)) {
throw new CloudRuntimeException(String.format(
"Cannot unregister extension '%s' from physical network %s. "
- + "Provider is used by %d existing network(s)",
+ + "Provider is used by %d existing network service(s)",
ext.getName(), physNetId, networksUsingProvider.size()));
}
}
@@ -1953,6 +1966,10 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
// 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) {
@@ -2061,9 +2078,9 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
parameters = ExtensionCustomAction.Parameter.validateParameterValues(actionParameters, cmdParameters);
}
- // Find the provider name for this network (try each service until we find one)
+ // Find the provider name for this network (try CustomAction first, then other services)
String providerName = null;
- for (Service service : new Service[]{Service.SourceNat, Service.StaticNat,
+ for (Service service : new Service[]{Service.CustomAction, Service.SourceNat, Service.StaticNat,
Service.PortForwarding, Service.Firewall, Service.Gateway}) {
providerName = networkServiceMapDao.getProviderForServiceInNetwork(network.getId(), service);
if (StringUtils.isNotBlank(providerName)) {
@@ -2115,6 +2132,91 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
return response;
}
+ /**
+ * Executes a custom action for a VPC resource by finding the VPC's
+ * extension provider and dispatching directly to it (no tier network lookup).
+ */
+ protected CustomActionResultResponse runVpcCustomAction(Vpc vpc,
+ 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, vpc));
+
+ // Resolve action parameters
+ List actionParameters = null;
+ Pair