|
141 | 141 | import com.cloud.network.element.NetworkElement; |
142 | 142 | import com.cloud.network.dao.PhysicalNetworkDao; |
143 | 143 | import com.cloud.network.dao.PhysicalNetworkVO; |
| 144 | +import com.cloud.network.vpc.Vpc; |
| 145 | +import com.cloud.network.vpc.dao.VpcServiceMapDao; |
144 | 146 | import com.cloud.org.Cluster; |
145 | 147 | import com.cloud.serializer.GsonHelper; |
146 | 148 | import com.cloud.storage.dao.VMTemplateDao; |
@@ -242,6 +244,9 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana |
242 | 244 | @Inject |
243 | 245 | NetworkServiceMapDao networkServiceMapDao; |
244 | 246 |
|
| 247 | + @Inject |
| 248 | + VpcServiceMapDao vpcServiceMapDao; |
| 249 | + |
245 | 250 | @Inject |
246 | 251 | NetworkModel networkModel; |
247 | 252 |
|
@@ -472,6 +477,19 @@ protected Extension getExtensionFromResource(ExtensionCustomAction.ResourceType |
472 | 477 | return null; |
473 | 478 | } |
474 | 479 | 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; |
475 | 493 | } |
476 | 494 | return null; |
477 | 495 | } |
@@ -1103,19 +1121,13 @@ public Extension updateRegisteredExtensionWithResource(UpdateRegisteredExtension |
1103 | 1121 |
|
1104 | 1122 | if (Boolean.TRUE.equals(cleanupDetails)) { |
1105 | 1123 | extensionResourceMapDetailsDao.removeDetails(targetMapping.getId()); |
1106 | | - } else { |
| 1124 | + } else if (MapUtils.isNotEmpty(details)) { |
1107 | 1125 | List<ExtensionResourceMapDetailsVO> detailsList = buildExtensionResourceDetailsArray(targetMapping.getId(), details); |
1108 | | - if (CollectionUtils.isNotEmpty(detailsList)) { |
1109 | | - appendHiddenExtensionResourceDetails(targetMapping.getId(), detailsList); |
1110 | | - } |
| 1126 | + appendHiddenExtensionResourceDetails(targetMapping.getId(), detailsList); |
1111 | 1127 | detailsList = detailsList.stream() |
1112 | 1128 | .filter(detail -> detail.getValue() != null) |
1113 | 1129 | .collect(Collectors.toList()); |
1114 | | - if (CollectionUtils.isNotEmpty(detailsList)) { |
1115 | | - extensionResourceMapDetailsDao.saveDetails(detailsList); |
1116 | | - } else { |
1117 | | - extensionResourceMapDetailsDao.removeDetails(targetMapping.getId()); |
1118 | | - } |
| 1130 | + extensionResourceMapDetailsDao.saveDetails(detailsList); |
1119 | 1131 | } |
1120 | 1132 |
|
1121 | 1133 | return extensionDao.findById(extensionId); |
@@ -1222,7 +1234,7 @@ protected ExtensionResourceMap registerExtensionWithPhysicalNetwork(PhysicalNetw |
1222 | 1234 | + "with services {}", extension.getName(), physicalNetwork.getId(), services); |
1223 | 1235 | } |
1224 | 1236 |
|
1225 | | - return extensionMap; |
| 1237 | + return savedExtensionMap; |
1226 | 1238 | }); |
1227 | 1239 | } |
1228 | 1240 |
|
@@ -1335,6 +1347,7 @@ private void applyServicesToNsp(PhysicalNetworkServiceProviderVO nsp, Set<String |
1335 | 1347 | nsp.setVpnServiceProvided(services.contains("Vpn")); |
1336 | 1348 | nsp.setSecuritygroupServiceProvided(services.contains("SecurityGroup")); |
1337 | 1349 | nsp.setNetworkAclServiceProvided(services.contains("NetworkACL")); |
| 1350 | + nsp.setCustomActionServiceProvided(services.contains("CustomAction")); |
1338 | 1351 | } |
1339 | 1352 |
|
1340 | 1353 | /** Keys that are always stored with display=false (sensitive). */ |
@@ -1463,7 +1476,7 @@ protected void unregisterExtensionWithPhysicalNetwork(String resourceId, Long ex |
1463 | 1476 | if (CollectionUtils.isNotEmpty(networksUsingProvider)) { |
1464 | 1477 | throw new CloudRuntimeException(String.format( |
1465 | 1478 | "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)", |
1467 | 1480 | ext.getName(), physNetId, networksUsingProvider.size())); |
1468 | 1481 | } |
1469 | 1482 | } |
@@ -1953,6 +1966,10 @@ public CustomActionResultResponse runCustomAction(RunCustomActionCmd cmd) { |
1953 | 1966 | // Network custom action: dispatched directly to NetworkCustomActionProvider (no agent) |
1954 | 1967 | Network network = (Network) entity; |
1955 | 1968 | 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); |
1956 | 1973 | } |
1957 | 1974 |
|
1958 | 1975 | if (clusterId == null || hostId == null) { |
@@ -2061,9 +2078,9 @@ protected CustomActionResultResponse runNetworkCustomAction(Network network, |
2061 | 2078 | parameters = ExtensionCustomAction.Parameter.validateParameterValues(actionParameters, cmdParameters); |
2062 | 2079 | } |
2063 | 2080 |
|
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) |
2065 | 2082 | 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, |
2067 | 2084 | Service.PortForwarding, Service.Firewall, Service.Gateway}) { |
2068 | 2085 | providerName = networkServiceMapDao.getProviderForServiceInNetwork(network.getId(), service); |
2069 | 2086 | if (StringUtils.isNotBlank(providerName)) { |
@@ -2115,6 +2132,91 @@ protected CustomActionResultResponse runNetworkCustomAction(Network network, |
2115 | 2132 | return response; |
2116 | 2133 | } |
2117 | 2134 |
|
| 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 | + |
2118 | 2220 | @Override |
2119 | 2221 | public ExtensionCustomActionResponse createCustomActionResponse(ExtensionCustomAction customAction) { |
2120 | 2222 | ExtensionCustomActionResponse response = new ExtensionCustomActionResponse(customAction.getUuid(), |
|
0 commit comments