Skip to content

Commit 193f885

Browse files
committed
network extension: add service CustomAction
1 parent 438c6c4 commit 193f885

File tree

15 files changed

+515
-50
lines changed

15 files changed

+515
-50
lines changed

api/src/main/java/com/cloud/network/Network.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ class Service {
116116
public static final Service NetworkACL = new Service("NetworkACL", Capability.SupportedProtocols);
117117
public static final Service Connectivity = new Service("Connectivity", Capability.DistributedRouter, Capability.RegionLevelVpc, Capability.StretchedL2Subnet,
118118
Capability.NoVlan, Capability.PublicAccess);
119+
public static final Service CustomAction = new Service("CustomAction");
119120

120121
private final String name;
121122
private final Capability[] caps;

api/src/main/java/org/apache/cloudstack/extension/ExtensionCustomAction.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@
4949
public interface ExtensionCustomAction extends InternalIdentity, Identity {
5050
enum ResourceType {
5151
VirtualMachine(com.cloud.vm.VirtualMachine.class),
52-
Network(com.cloud.network.Network.class);
52+
Network(com.cloud.network.Network.class),
53+
Vpc(com.cloud.network.vpc.Vpc.class);
5354

5455
private final Class<?> clazz;
5556

api/src/main/java/org/apache/cloudstack/extension/NetworkCustomActionProvider.java

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,15 @@
2020
import java.util.Map;
2121

2222
import com.cloud.network.Network;
23+
import com.cloud.network.vpc.Vpc;
2324

2425
/**
2526
* Implemented by network elements that support running custom actions on a
26-
* managed network (e.g. NetworkExtensionElement).
27+
* managed network or VPC (e.g. NetworkExtensionElement).
2728
*
2829
* <p>This interface is looked up by {@code ExtensionsManagerImpl} to dispatch
29-
* {@code runCustomAction} requests whose resource type is {@code Network}.</p>
30+
* {@code runCustomAction} requests whose resource type is {@code Network}
31+
* or {@code Vpc}.</p>
3032
*/
3133
public interface NetworkCustomActionProvider {
3234

@@ -39,6 +41,15 @@ public interface NetworkCustomActionProvider {
3941
*/
4042
boolean canHandleCustomAction(Network network);
4143

44+
/**
45+
* Returns {@code true} if this provider can handle custom actions for
46+
* the given VPC.
47+
*
48+
* @param vpc the target VPC
49+
* @return {@code true} if this provider can handle the VPC
50+
*/
51+
boolean canHandleVpcCustomAction(Vpc vpc);
52+
4253
/**
4354
* Runs a named custom action against the external network device that
4455
* manages the given network.
@@ -49,4 +60,15 @@ public interface NetworkCustomActionProvider {
4960
* @return output from the action script, or {@code null} on failure
5061
*/
5162
String runCustomAction(Network network, String actionName, Map<String, Object> parameters);
63+
64+
/**
65+
* Runs a named custom action against the external network device that
66+
* manages the given VPC.
67+
*
68+
* @param vpc the CloudStack VPC on which to run the action
69+
* @param actionName the action name
70+
* @param parameters optional parameters supplied by the caller
71+
* @return output from the action script, or {@code null} on failure
72+
*/
73+
String runCustomAction(Vpc vpc, String actionName, Map<String, Object> parameters);
5274
}

engine/schema/src/main/java/com/cloud/network/dao/PhysicalNetworkServiceProviderVO.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ public class PhysicalNetworkServiceProviderVO implements PhysicalNetworkServiceP
9797
@Column(name = "networkacl_service_provided")
9898
boolean networkAclServiceProvided;
9999

100+
@Column(name = "custom_action_service_provided")
101+
boolean customActionServiceProvided;
102+
100103
@Column(name = GenericDao.REMOVED_COLUMN)
101104
Date removed;
102105

@@ -278,6 +281,7 @@ public void setEnabledServices(List<Service> services) {
278281
this.setUserdataServiceProvided(services.contains(Service.UserData));
279282
this.setSecuritygroupServiceProvided(services.contains(Service.SecurityGroup));
280283
this.setNetworkAclServiceProvided(services.contains(Service.NetworkACL));
284+
this.setCustomActionServiceProvided(services.contains(Service.CustomAction));
281285
}
282286

283287
@Override
@@ -316,6 +320,9 @@ public List<Service> getEnabledServices() {
316320
if (this.isSecuritygroupServiceProvided()) {
317321
services.add(Service.SecurityGroup);
318322
}
323+
if (this.isCustomActionServiceProvided()) {
324+
services.add(Service.CustomAction);
325+
}
319326
return services;
320327
}
321328

@@ -327,4 +334,12 @@ public boolean isNetworkAclServiceProvided() {
327334
public void setNetworkAclServiceProvided(boolean networkAclServiceProvided) {
328335
this.networkAclServiceProvided = networkAclServiceProvided;
329336
}
337+
338+
public boolean isCustomActionServiceProvided() {
339+
return customActionServiceProvided;
340+
}
341+
342+
public void setCustomActionServiceProvided(boolean customActionServiceProvided) {
343+
this.customActionServiceProvided = customActionServiceProvided;
344+
}
330345
}

engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,6 @@ CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.nics','enabled', 'TINYINT(1) NOT NUL
121121
-- Increase length of value of extension details from 255 to 4096 to support longer details value
122122
CALL `cloud`.`IDEMPOTENT_CHANGE_COLUMN`('cloud.extension_details', 'value', 'value', 'VARCHAR(4096)');
123123
CALL `cloud`.`IDEMPOTENT_CHANGE_COLUMN`('cloud.extension_resource_map_details', 'value', 'value', 'VARCHAR(4096)');
124+
125+
-- Add CustomAction service support to physical_network_service_providers
126+
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"');

framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/ListExtensionsCmd.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,15 +70,15 @@ public class ListExtensionsCmd extends BaseListCmd {
7070
+ " When no parameters are passed, all the details are returned.")
7171
private List<String> details;
7272

73-
@Parameter(name = ApiConstants.TYPE, type = CommandType.STRING, description = "Type of the extension (e.g. Orchestrator, NetworkOrchestrator). Default is Orchestrator if not set")
73+
@Parameter(name = ApiConstants.TYPE, type = CommandType.STRING, description = "Type of the extension (e.g. Orchestrator, NetworkOrchestrator)")
7474
private String type;
7575

7676
@Parameter(name = ApiConstants.RESOURCE_ID, type = CommandType.STRING,
7777
description = "ID of the resource to list registered extensions for (e.g. cluster UUID, physical network UUID)")
7878
private String resourceId;
7979

8080
@Parameter(name = ApiConstants.RESOURCE_TYPE, type = CommandType.STRING,
81-
description = "Type of the resource (e.g. Cluster, PhysicalNetwork). Default is Cluster if not set")
81+
description = "Type of the resource (e.g. Cluster, PhysicalNetwork)")
8282
private String resourceType;
8383

8484
/////////////////////////////////////////////////////

framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManagerImpl.java

Lines changed: 115 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@
141141
import com.cloud.network.element.NetworkElement;
142142
import com.cloud.network.dao.PhysicalNetworkDao;
143143
import com.cloud.network.dao.PhysicalNetworkVO;
144+
import com.cloud.network.vpc.Vpc;
145+
import com.cloud.network.vpc.dao.VpcServiceMapDao;
144146
import com.cloud.org.Cluster;
145147
import com.cloud.serializer.GsonHelper;
146148
import com.cloud.storage.dao.VMTemplateDao;
@@ -242,6 +244,9 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
242244
@Inject
243245
NetworkServiceMapDao networkServiceMapDao;
244246

247+
@Inject
248+
VpcServiceMapDao vpcServiceMapDao;
249+
245250
@Inject
246251
NetworkModel networkModel;
247252

@@ -472,6 +477,19 @@ protected Extension getExtensionFromResource(ExtensionCustomAction.ResourceType
472477
return null;
473478
}
474479
return extensionDao.findById(maps.get(0).getExtensionId());
480+
} else if (resourceType == ExtensionCustomAction.ResourceType.Vpc) {
481+
com.cloud.network.vpc.Vpc vpc = (com.cloud.network.vpc.Vpc) object;
482+
// Find extension via the VPC's tier networks
483+
List<NetworkVO> tierNetworks = networkDao.listByVpc(vpc.getId());
484+
if (CollectionUtils.isNotEmpty(tierNetworks)) {
485+
for (NetworkVO tierNetwork : tierNetworks) {
486+
Extension ext = getExtensionFromResource(ExtensionCustomAction.ResourceType.Network, tierNetwork.getUuid());
487+
if (ext != null) {
488+
return ext;
489+
}
490+
}
491+
}
492+
return null;
475493
}
476494
return null;
477495
}
@@ -1103,19 +1121,13 @@ public Extension updateRegisteredExtensionWithResource(UpdateRegisteredExtension
11031121

11041122
if (Boolean.TRUE.equals(cleanupDetails)) {
11051123
extensionResourceMapDetailsDao.removeDetails(targetMapping.getId());
1106-
} else {
1124+
} else if (MapUtils.isNotEmpty(details)) {
11071125
List<ExtensionResourceMapDetailsVO> detailsList = buildExtensionResourceDetailsArray(targetMapping.getId(), details);
1108-
if (CollectionUtils.isNotEmpty(detailsList)) {
1109-
appendHiddenExtensionResourceDetails(targetMapping.getId(), detailsList);
1110-
}
1126+
appendHiddenExtensionResourceDetails(targetMapping.getId(), detailsList);
11111127
detailsList = detailsList.stream()
11121128
.filter(detail -> detail.getValue() != null)
11131129
.collect(Collectors.toList());
1114-
if (CollectionUtils.isNotEmpty(detailsList)) {
1115-
extensionResourceMapDetailsDao.saveDetails(detailsList);
1116-
} else {
1117-
extensionResourceMapDetailsDao.removeDetails(targetMapping.getId());
1118-
}
1130+
extensionResourceMapDetailsDao.saveDetails(detailsList);
11191131
}
11201132

11211133
return extensionDao.findById(extensionId);
@@ -1222,7 +1234,7 @@ protected ExtensionResourceMap registerExtensionWithPhysicalNetwork(PhysicalNetw
12221234
+ "with services {}", extension.getName(), physicalNetwork.getId(), services);
12231235
}
12241236

1225-
return extensionMap;
1237+
return savedExtensionMap;
12261238
});
12271239
}
12281240

@@ -1335,6 +1347,7 @@ private void applyServicesToNsp(PhysicalNetworkServiceProviderVO nsp, Set<String
13351347
nsp.setVpnServiceProvided(services.contains("Vpn"));
13361348
nsp.setSecuritygroupServiceProvided(services.contains("SecurityGroup"));
13371349
nsp.setNetworkAclServiceProvided(services.contains("NetworkACL"));
1350+
nsp.setCustomActionServiceProvided(services.contains("CustomAction"));
13381351
}
13391352

13401353
/** Keys that are always stored with display=false (sensitive). */
@@ -1463,7 +1476,7 @@ protected void unregisterExtensionWithPhysicalNetwork(String resourceId, Long ex
14631476
if (CollectionUtils.isNotEmpty(networksUsingProvider)) {
14641477
throw new CloudRuntimeException(String.format(
14651478
"Cannot unregister extension '%s' from physical network %s. "
1466-
+ "Provider is used by %d existing network(s)",
1479+
+ "Provider is used by %d existing network service(s)",
14671480
ext.getName(), physNetId, networksUsingProvider.size()));
14681481
}
14691482
}
@@ -1953,6 +1966,10 @@ public CustomActionResultResponse runCustomAction(RunCustomActionCmd cmd) {
19531966
// Network custom action: dispatched directly to NetworkCustomActionProvider (no agent)
19541967
Network network = (Network) entity;
19551968
return runNetworkCustomAction(network, customActionVO, extensionVO, actionResourceType, cmdParameters);
1969+
} else if (entity instanceof Vpc) {
1970+
// VPC custom action: find a tier network and dispatch to the same NetworkCustomActionProvider
1971+
Vpc vpc = (Vpc) entity;
1972+
return runVpcCustomAction(vpc, customActionVO, extensionVO, actionResourceType, cmdParameters);
19561973
}
19571974

19581975
if (clusterId == null || hostId == null) {
@@ -2061,9 +2078,9 @@ protected CustomActionResultResponse runNetworkCustomAction(Network network,
20612078
parameters = ExtensionCustomAction.Parameter.validateParameterValues(actionParameters, cmdParameters);
20622079
}
20632080

2064-
// Find the provider name for this network (try each service until we find one)
2081+
// Find the provider name for this network (try CustomAction first, then other services)
20652082
String providerName = null;
2066-
for (Service service : new Service[]{Service.SourceNat, Service.StaticNat,
2083+
for (Service service : new Service[]{Service.CustomAction, Service.SourceNat, Service.StaticNat,
20672084
Service.PortForwarding, Service.Firewall, Service.Gateway}) {
20682085
providerName = networkServiceMapDao.getProviderForServiceInNetwork(network.getId(), service);
20692086
if (StringUtils.isNotBlank(providerName)) {
@@ -2115,6 +2132,91 @@ protected CustomActionResultResponse runNetworkCustomAction(Network network,
21152132
return response;
21162133
}
21172134

2135+
/**
2136+
* Executes a custom action for a VPC resource by finding the VPC's
2137+
* extension provider and dispatching directly to it (no tier network lookup).
2138+
*/
2139+
protected CustomActionResultResponse runVpcCustomAction(Vpc vpc,
2140+
ExtensionCustomActionVO customActionVO, ExtensionVO extensionVO,
2141+
ExtensionCustomAction.ResourceType actionResourceType,
2142+
Map<String, String> cmdParameters) {
2143+
2144+
final String actionName = customActionVO.getName();
2145+
CustomActionResultResponse response = new CustomActionResultResponse();
2146+
response.setId(customActionVO.getUuid());
2147+
response.setName(actionName);
2148+
response.setObjectName("customactionresult");
2149+
Map<String, String> result = new HashMap<>();
2150+
response.setSuccess(false);
2151+
result.put(ApiConstants.MESSAGE, getActionMessage(false, customActionVO, extensionVO, actionResourceType, vpc));
2152+
2153+
// Resolve action parameters
2154+
List<ExtensionCustomAction.Parameter> actionParameters = null;
2155+
Pair<Map<String, String>, Map<String, String>> allDetails =
2156+
extensionCustomActionDetailsDao.listDetailsKeyPairsWithVisibility(customActionVO.getId());
2157+
if (allDetails.second().containsKey(ApiConstants.PARAMETERS)) {
2158+
actionParameters = ExtensionCustomAction.Parameter.toListFromJson(
2159+
allDetails.second().get(ApiConstants.PARAMETERS));
2160+
}
2161+
Map<String, Object> parameters = null;
2162+
if (CollectionUtils.isNotEmpty(actionParameters)) {
2163+
parameters = ExtensionCustomAction.Parameter.validateParameterValues(actionParameters, cmdParameters);
2164+
}
2165+
2166+
// Find the provider name for this VPC
2167+
String providerName = null;
2168+
for (Service service : new Service[]{Service.CustomAction, Service.SourceNat, Service.StaticNat,
2169+
Service.PortForwarding, Service.NetworkACL, Service.Gateway}) {
2170+
providerName = vpcServiceMapDao.getProviderForServiceInVpc(vpc.getId(), service);
2171+
if (StringUtils.isNotBlank(providerName)) {
2172+
break;
2173+
}
2174+
}
2175+
if (StringUtils.isBlank(providerName)) {
2176+
logger.error("No VPC service provider found for VPC {}", vpc.getId());
2177+
result.put(ApiConstants.DETAILS, "No VPC service provider found for this VPC");
2178+
response.setResult(result);
2179+
return response;
2180+
}
2181+
2182+
// Get the network element implementing that provider
2183+
NetworkElement element = networkModel.getElementImplementingProvider(providerName);
2184+
if (element == null) {
2185+
logger.error("No NetworkElement found implementing provider '{}' for VPC {}", providerName, vpc.getId());
2186+
result.put(ApiConstants.DETAILS, "No network element found for provider: " + providerName);
2187+
response.setResult(result);
2188+
return response;
2189+
}
2190+
2191+
// The element must implement NetworkCustomActionProvider
2192+
if (!(element instanceof NetworkCustomActionProvider)) {
2193+
logger.error("Network element '{}' for provider '{}' does not support VPC custom actions",
2194+
element.getClass().getSimpleName(), providerName);
2195+
result.put(ApiConstants.DETAILS, "Provider '" + providerName + "' does not support custom actions");
2196+
response.setResult(result);
2197+
return response;
2198+
}
2199+
2200+
NetworkCustomActionProvider provider = (NetworkCustomActionProvider) element;
2201+
try {
2202+
if (!provider.canHandleVpcCustomAction(vpc)) {
2203+
throw new CloudRuntimeException("Provider '" + providerName + "' cannot handle custom action for this VPC");
2204+
}
2205+
logger.info("Running VPC custom action '{}' on VPC {} via {} (provider: {})",
2206+
actionName, vpc.getId(), element.getClass().getSimpleName(), providerName);
2207+
String output = provider.runCustomAction(vpc, actionName, parameters);
2208+
boolean success = output != null;
2209+
response.setSuccess(success);
2210+
result.put(ApiConstants.MESSAGE, getActionMessage(success, customActionVO, extensionVO, actionResourceType, vpc));
2211+
result.put(ApiConstants.DETAILS, success ? output : "Action failed — check management server logs for details");
2212+
} catch (Exception e) {
2213+
logger.error("VPC custom action '{}' threw exception: {}", actionName, e.getMessage(), e);
2214+
result.put(ApiConstants.DETAILS, "Action failed: " + e.getMessage());
2215+
}
2216+
response.setResult(result);
2217+
return response;
2218+
}
2219+
21182220
@Override
21192221
public ExtensionCustomActionResponse createCustomActionResponse(ExtensionCustomAction customAction) {
21202222
ExtensionCustomActionResponse response = new ExtensionCustomActionResponse(customAction.getUuid(),

0 commit comments

Comments
 (0)