Network Extension: Orchestrate external Network devices

This commit is contained in:
Wei Zhou 2026-04-15 08:55:48 +02:00
parent 5893ba5a8c
commit b39eeac0d7
69 changed files with 9175 additions and 219 deletions

View File

@ -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";

View File

@ -207,6 +207,7 @@ public interface Network extends ControlledEntity, StateObject<Network.State>, I
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 +251,47 @@ public interface Network extends ControlledEntity, StateObject<Network.State>, I
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.
*
* <p>The new instance is <em>not</em> 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).</p>
*
* @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 {

View File

@ -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

View File

@ -155,7 +155,7 @@ public interface NetworkService {
Pair<List<? extends PhysicalNetwork>, Integer> searchPhysicalNetworks(Long id, Long zoneId, String keyword, Long startIndex, Long pageSize, String name);
PhysicalNetwork updatePhysicalNetwork(Long id, String networkSpeed, List<String> tags, String newVnetRangeString, String state);
PhysicalNetwork updatePhysicalNetwork(Long id, String networkSpeed, List<String> tags, String newVnetRangeString, String state, Map<String, String> externalDetails);
boolean deletePhysicalNetwork(Long id);

View File

@ -146,4 +146,8 @@ public interface NetworkElement extends Adapter {
* @return true/false
*/
boolean verifyServicesCombination(Set<Service> services);
default boolean rollingRestartSupported() {
return true;
}
}

View File

@ -17,6 +17,7 @@
package org.apache.cloudstack.api.command.admin.network;
import java.util.List;
import java.util.Map;
import org.apache.cloudstack.api.APICommand;
@ -53,6 +54,12 @@ public class UpdatePhysicalNetworkCmd extends BaseAsyncCmd {
@Parameter(name = ApiConstants.VLAN, type = CommandType.STRING, description = "The VLAN for the physical Network")
private String vlan;
@Parameter(name = ApiConstants.EXTERNAL_DETAILS,
type = CommandType.MAP,
description = "Details in key/value pairs to be added to the extension-resource mapping. Use the format externaldetails[i].<key>=<value>. Example: externaldetails[0].endpoint.url=https://example.com",
since = "4.23.0")
protected Map externalDetails;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
@ -77,6 +84,10 @@ public class UpdatePhysicalNetworkCmd extends BaseAsyncCmd {
return vlan;
}
public Map<String, String> getExternalDetails() {
return convertDetailsToMap(externalDetails);
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
@ -88,7 +99,7 @@ public class UpdatePhysicalNetworkCmd extends BaseAsyncCmd {
@Override
public void execute() {
PhysicalNetwork result = _networkService.updatePhysicalNetwork(getId(), getNetworkSpeed(), getTags(), getVlan(), getState());
PhysicalNetwork result = _networkService.updatePhysicalNetwork(getId(), getNetworkSpeed(), getTags(), getVlan(), getState(), getExternalDetails());
if (result != null) {
PhysicalNetworkResponse response = _responseGenerator.createPhysicalNetworkResponse(result);
response.setResponseName(getCommandName());

View File

@ -62,4 +62,12 @@ public class CustomActionResultResponse extends BaseResponse {
public void setResult(Map<String, String> result) {
this.result = result;
}
public Map<String, String> getResult() {
return result;
}
public boolean isSuccess() {
return Boolean.TRUE.equals(success);
}
}

View File

@ -24,7 +24,8 @@ import org.apache.cloudstack.api.InternalIdentity;
public interface Extension extends InternalIdentity, Identity {
enum Type {
Orchestrator
Orchestrator,
NetworkOrchestrator
}
enum State {
Enabled, Disabled;

View File

@ -48,7 +48,8 @@ import com.google.gson.reflect.TypeToken;
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);
private final Class<?> clazz;

View File

@ -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<String> 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<String, String> 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.
*
* <p>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.</p>
*
* @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<String, String> 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<Extension> 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.
*
* <p>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.</p>
*
* @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<Service, Map<Capability, String>> getNetworkCapabilitiesForProvider(Long physicalNetworkId, String providerName);
}

View File

@ -24,7 +24,8 @@ import org.apache.cloudstack.api.InternalIdentity;
public interface ExtensionResourceMap extends InternalIdentity, Identity {
enum ResourceType {
Cluster
Cluster,
PhysicalNetwork
}
long getExtensionId();

View File

@ -0,0 +1,53 @@
// 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;
/**
* Implemented by network elements that support running custom actions on a
* managed network (e.g. NetworkExtensionElement).
*
* <p>This interface is looked up by {@code ExtensionsManagerImpl} to dispatch
* {@code runCustomAction} requests whose resource type is {@code Network}.</p>
*/
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);
/**
* 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<String, Object> parameters);
}

View File

@ -0,0 +1,40 @@
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.assertTrue;
public class NetworkTest {
@Test
public void testProviderContains() {
List<Network.Provider> 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));
}
}

View File

@ -40,6 +40,12 @@ public class ExtensionCustomActionTest {
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());

View File

@ -177,6 +177,10 @@ import com.cloud.network.dao.PhysicalNetworkVO;
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<NetworkGuru> getNetworkGurus() {
@ -461,6 +469,28 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra
HashMap<Long, Long> _lastNetworkIdsToFree = new HashMap<>();
/**
* Returns the full list of network elements to iterate when implementing,
* shutting down, or otherwise orchestrating a network.
*
* <p>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.</p>
*/
private List<NetworkElement> getNetworkElementsIncludingExtensions() {
List<Extension> extensions = extensionHelper.listExtensionsByType(Extension.Type.NetworkOrchestrator);
if (extensions == null || extensions.isEmpty()) {
return networkElements;
}
List<NetworkElement> 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;
@ -1686,7 +1716,7 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra
}
}
for (final NetworkElement element : networkElements) {
for (final NetworkElement element : getNetworkElementsIncludingExtensions()) {
if (element instanceof AggregatedCommandExecutor && providersToImplement.contains(element.getProvider())) {
((AggregatedCommandExecutor) element).prepareAggregatedExecution(network, dest);
}
@ -1703,7 +1733,7 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra
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);
@ -1717,7 +1747,7 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra
}
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);
}
@ -1738,7 +1768,7 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra
private void implementNetworkElements(final DeployDestination dest, final ReservationContext context, final Network network, final NetworkOffering offering, final List<Provider> 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,
@ -2031,7 +2061,7 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra
@Override
public void configureUpdateInSequence(Network network) {
List<Provider> 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);
@ -2044,7 +2074,7 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra
public int getResourceCount(Network network) {
List<Provider> 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) {
@ -2075,7 +2105,7 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra
@Override
public void finalizeUpdateInSequence(Network network, boolean success) {
List<Provider> 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) {
@ -2102,7 +2132,7 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra
}
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, " +
@ -2230,7 +2260,7 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra
updateNic(nic, network, 1);
final List<Provider> 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: "
@ -2285,7 +2315,7 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra
}
final List<Provider> 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: "
@ -2329,7 +2359,7 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra
}
}
final List<Provider> 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",
@ -2411,7 +2441,7 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra
}
final List<Provider> 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: "
@ -2447,7 +2477,7 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra
}
final List<Provider> 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: "
@ -2534,7 +2564,7 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra
final Network network = networkToRelease.first();
final NicProfile profile = networkToRelease.second();
final List<Provider> 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
@ -2597,7 +2627,7 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra
*/
if (nic.getReservationStrategy() == Nic.ReservationStrategy.Create) {
final List<Provider> 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 {
@ -3328,7 +3358,7 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra
// 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());
@ -3439,7 +3469,7 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra
// get providers to destroy
final List<Provider> 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);
@ -3810,7 +3840,7 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra
public void cleanupNicDhcpDnsEntry(Network network, VirtualMachineProfile vmProfile, NicProfile nicProfile) {
final List<Provider> 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: "
@ -3846,7 +3876,7 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra
* @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;
@ -3894,6 +3924,20 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra
return areRoutersRunning(routerDao.findByNetwork(network.getId()));
}
private boolean isRollingRestartSupport(final NetworkVO network) {
if (!NetworkOrchestrator.RollingRestartEnabled.value()) {
return false;
}
List<NetworkServiceMapVO> 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);
@ -4455,6 +4499,8 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra
if (provider == null) {
provider = _networkModel.getDefaultUniqueProviderForService(service).getName();
} else {
provider = _networkModel.resolveProvider(provider).getName();
}
// check that provider is supported
@ -4480,7 +4526,7 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra
final List<String> providerNames = _ntwkSrvcDao.getDistinctProviders(networkId);
final List<Provider> providers = new ArrayList<>();
for (final String providerName : providerNames) {
providers.add(Network.Provider.getProvider(providerName));
providers.add(_networkModel.resolveProvider(providerName));
}
return providers;
@ -4646,7 +4692,7 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra
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;
@ -4931,10 +4977,10 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra
@Override
public void expungeLbVmRefs(List<Long> vmIds, Long batchSize) {
if (CollectionUtils.isEmpty(networkElements) || CollectionUtils.isEmpty(vmIds)) {
if (CollectionUtils.isEmpty(getNetworkElementsIncludingExtensions()) || CollectionUtils.isEmpty(vmIds)) {
return;
}
for (NetworkElement element : networkElements) {
for (NetworkElement element : getNetworkElementsIncludingExtensions()) {
if (element instanceof LoadBalancingServiceProvider) {
LoadBalancingServiceProvider lbProvider = (LoadBalancingServiceProvider)element;
lbProvider.expungeLbVmRefs(vmIds, batchSize);

View File

@ -35,6 +35,7 @@ import com.cloud.dc.DataCenter;
import com.cloud.exception.InsufficientVirtualNetworkCapacityException;
import com.cloud.network.IpAddressManager;
import com.cloud.utils.Pair;
import org.apache.cloudstack.extension.ExtensionHelper;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
@ -135,6 +136,7 @@ public class NetworkOrchestratorTest extends TestCase {
testOrchestrator.routerJoinDao = mock(DomainRouterJoinDao.class);
testOrchestrator._ipAddrMgr = mock(IpAddressManager.class);
testOrchestrator._entityMgr = mock(EntityManager.class);
testOrchestrator.extensionHelper = mock(ExtensionHelper.class);
DhcpServiceProvider provider = mock(DhcpServiceProvider.class);
Map<Network.Capability, String> capabilities = new HashMap<Network.Capability, String>();

View File

@ -37,7 +37,6 @@ import org.springframework.stereotype.Component;
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 class NetworkDaoImpl extends GenericDaoBase<NetworkVO, Long>implements Ne
final TransactionLegacy txn = TransactionLegacy.currentTxn();
txn.start();
for (final String service : serviceProviderMap.keySet()) {
final NetworkServiceMapVO serviceMap = new NetworkServiceMapVO(networkId, Service.getService(service), Provider.getProvider(serviceProviderMap.get(service)));
final NetworkServiceMapVO serviceMap = new NetworkServiceMapVO(networkId, Service.getService(service).getName(), serviceProviderMap.get(service));
_ntwkSvcMap.persist(serviceMap);
}
txn.commit();

View File

@ -81,6 +81,12 @@ public class NetworkServiceMapVO implements InternalIdentity {
this.provider = provider.getName();
}
public NetworkServiceMapVO(long networkId, String serviceName, String providerName) {
this.networkId = networkId;
this.service = serviceName;
this.provider = providerName;
}
@Override
public String toString() {
StringBuilder buf = new StringBuilder("[Network Service[");

View File

@ -25,8 +25,6 @@ import javax.persistence.GenerationType;
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 class VpcServiceMapVO {
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

View File

@ -142,7 +142,7 @@ public class VpcDaoImpl extends GenericDaoBase<VpcVO, Long> implements VpcDao {
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);
}
}

View File

@ -131,3 +131,7 @@ CREATE TABLE IF NOT EXISTS `cloud_usage`.`quota_tariff_usage` (
-- Add the 'keep_mac_address_on_public_nic' column to the 'cloud.networks' and 'cloud.vpc' tables
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.networks', 'keep_mac_address_on_public_nic', 'TINYINT(1) NOT NULL DEFAULT 1');
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.vpc', 'keep_mac_address_on_public_nic', 'TINYINT(1) NOT NULL DEFAULT 1');
-- 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)');

View File

@ -70,6 +70,17 @@ public class ListExtensionsCmd extends BaseListCmd {
+ " When no parameters are passed, all the details are returned.")
private List<String> details;
@Parameter(name = ApiConstants.TYPE, type = CommandType.STRING, description = "Type of the extension (e.g. Orchestrator, NetworkOrchestrator). Default is Orchestrator if not set")
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). Default is Cluster if not set")
private String resourceType;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
@ -82,6 +93,18 @@ public class ListExtensionsCmd extends BaseListCmd {
return extensionId;
}
public String getType() {
return type;
}
public String getResourceId() {
return resourceId;
}
public String getResourceType() {
return resourceType;
}
public EnumSet<ApiConstants.ExtensionDetails> getDetails() throws InvalidParameterValueException {
if (CollectionUtils.isEmpty(details)) {
return EnumSet.of(ApiConstants.ExtensionDetails.all);

View File

@ -0,0 +1,117 @@
// 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<String, String> 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();
}
}

View File

@ -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 @@ import com.cloud.utils.db.GenericDao;
public interface ExtensionDao extends GenericDao<ExtensionVO, Long> {
ExtensionVO findByName(String name);
List<ExtensionVO> listByType(Extension.Type type);
}

View File

@ -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 class ExtensionDaoImpl extends GenericDaoBase<ExtensionVO, Long> implemen
public ExtensionVO findByName(String name) {
SearchCriteria<ExtensionVO> sc = AllFieldSearch.create();
sc.setParameters("name", name);
return findOneBy(sc);
}
@Override
public List<ExtensionVO> listByType(Extension.Type type) {
SearchCriteria<ExtensionVO> sc = AllFieldSearch.create();
sc.setParameters("type", type);
return listBy(sc);
}
}

View File

@ -28,5 +28,9 @@ public interface ExtensionResourceMapDao extends GenericDao<ExtensionResourceMap
ExtensionResourceMapVO findByResourceIdAndType(long resourceId, ExtensionResourceMap.ResourceType resourceType);
List<Long> listResourceIdsByExtensionIdAndType(long extensionId,ExtensionResourceMap.ResourceType resourceType);
List<ExtensionResourceMapVO> listByResourceIdAndType(long resourceId, ExtensionResourceMap.ResourceType resourceType);
List<Long> listResourceIdsByExtensionIdAndType(long extensionId, ExtensionResourceMap.ResourceType resourceType);
List<Long> listResourceIdsByType(ExtensionResourceMap.ResourceType resourceType);
}

View File

@ -55,6 +55,15 @@ public class ExtensionResourceMapDaoImpl extends GenericDaoBase<ExtensionResourc
return findOneBy(sc);
}
@Override
public List<ExtensionResourceMapVO> listByResourceIdAndType(long resourceId,
ExtensionResourceMap.ResourceType resourceType) {
SearchCriteria<ExtensionResourceMapVO> sc = genericSearch.create();
sc.setParameters("resourceId", resourceId);
sc.setParameters("resourceType", resourceType);
return listBy(sc);
}
@Override
public List<Long> listResourceIdsByExtensionIdAndType(long extensionId, ExtensionResourceMap.ResourceType resourceType) {
GenericSearchBuilder<ExtensionResourceMapVO, Long> sb = createSearchBuilder(Long.class);
@ -67,4 +76,15 @@ public class ExtensionResourceMapDaoImpl extends GenericDaoBase<ExtensionResourc
sc.setParameters("resourceType", resourceType);
return customSearch(sc, null);
}
@Override
public List<Long> listResourceIdsByType(ExtensionResourceMap.ResourceType resourceType) {
GenericSearchBuilder<ExtensionResourceMapVO, Long> sb = createSearchBuilder(Long.class);
sb.selectFields(sb.entity().getResourceId());
sb.and("resourceType", sb.entity().getResourceType(), SearchCriteria.Op.EQ);
sb.done();
SearchCriteria<Long> sc = sb.create();
sc.setParameters("resourceType", resourceType);
return customSearch(sc, null);
}
}

View File

@ -42,6 +42,7 @@ import org.apache.cloudstack.framework.extensions.api.RunCustomActionCmd;
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);

View File

@ -26,6 +26,7 @@ import java.nio.file.Path;
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.CustomActionResultResponse;
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.RunCustomActionCmd;
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,19 @@ import com.cloud.host.dao.HostDao;
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.org.Cluster;
import com.cloud.serializer.GsonHelper;
import com.cloud.storage.dao.VMTemplateDao;
@ -141,6 +157,10 @@ import com.cloud.utils.db.SearchBuilder;
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 +191,12 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
@Inject
ClusterDao clusterDao;
@Inject
PhysicalNetworkDao physicalNetworkDao;
@Inject
PhysicalNetworkServiceProviderDao physicalNetworkServiceProviderDao;
@Inject
AgentManager agentMgr;
@ -210,6 +236,15 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
@Inject
VMTemplateDao templateDao;
@Inject
NetworkDao networkDao;
@Inject
NetworkServiceMapDao networkServiceMapDao;
@Inject
NetworkModel networkModel;
@Inject
RoleService roleService;
@ -339,6 +374,39 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
return getResultFromAnswersString(answersStr, extension, msHost, "get path checksum");
}
protected List<ExtensionResourceMapDetailsVO> buildExtensionResourceDetailsArray(long extensionResourceMapId,
Map<String, String> details) {
List<ExtensionResourceMapDetailsVO> detailsList = new ArrayList<>();
if (MapUtils.isEmpty(details)) {
return detailsList;
}
for (Map.Entry<String, String> 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<ExtensionResourceMapDetailsVO> detailsList) {
if (CollectionUtils.isEmpty(detailsList)) {
return;
}
Map<String, String> hiddenDetails = extensionResourceMapDetailsDao.listDetailsKeyPairs(extensionResourceMapId, false);
if (MapUtils.isEmpty(hiddenDetails)) {
return;
}
Set<String> 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<ExtensionCustomAction.Parameter> getParametersListFromMap(String actionName, Map parametersMap) {
if (MapUtils.isEmpty(parametersMap)) {
return Collections.emptyList();
@ -370,16 +438,42 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
VirtualMachine vm = (VirtualMachine) object;
Pair<Long, Long> clusterHostId = virtualMachineManager.findClusterAndHostIdForVm(vm, false);
clusterId = clusterHostId.first();
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<String> providers = networkServiceMapDao.getDistinctProviders(network.getId());
if (CollectionUtils.isNotEmpty(providers)) {
for (String providerName : providers) {
Extension ext = getExtensionForPhysicalNetworkAndProvider(physicalNetworkId, providerName);
if (ext != null) {
return ext;
}
}
}
// Fallback: return the first extension registered on the physical network
List<ExtensionResourceMapVO> maps = extensionResourceMapDao.listByResourceIdAndType(
physicalNetworkId, ExtensionResourceMap.ResourceType.PhysicalNetwork);
if (CollectionUtils.isEmpty(maps)) {
return null;
}
return extensionDao.findById(maps.get(0).getExtensionId());
}
if (clusterId == null) {
return null;
}
ExtensionResourceMapVO mapVO =
extensionResourceMapDao.findByResourceIdAndType(clusterId, ExtensionResourceMap.ResourceType.Cluster);
if (mapVO == null) {
return null;
}
return extensionDao.findById(mapVO.getExtensionId());
return null;
}
protected String getActionMessage(boolean success, ExtensionCustomAction action, Extension extension,
@ -694,25 +788,68 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
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<ExtensionResourceMapVO> maps = extensionResourceMapDao.listByResourceIdAndType(resolvedResourceId, resType);
List<ExtensionResponse> 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<ExtensionVO> 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<ExtensionVO> 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<List<ExtensionVO>, Integer> result = extensionDao.searchAndCount(sc, searchFilter);
List<ExtensionResponse> responses = new ArrayList<>();
@ -880,21 +1017,110 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
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)));
}
ClusterVO clusterVO = clusterDao.findByUuid(resourceId);
if (clusterVO == null) {
throw new InvalidParameterValueException("Invalid cluster ID specified");
}
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<String, String> 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.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<ExtensionResourceMapVO> 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 {
List<ExtensionResourceMapDetailsVO> detailsList = buildExtensionResourceDetailsArray(targetMapping.getId(), details);
if (CollectionUtils.isNotEmpty(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());
}
}
return extensionDao.findById(extensionId);
}
@Override
@ActionEvent(eventType = EventTypes.EVENT_EXTENSION_RESOURCE_REGISTER, eventDescription = "registering extension resource")
public ExtensionResourceMap registerExtensionWithCluster(Cluster cluster, Extension extension,
@ -923,8 +1149,9 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
List<ExtensionResourceMapDetailsVO> detailsVOList = new ArrayList<>();
if (MapUtils.isNotEmpty(details)) {
for (Map.Entry<String, String> 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 +1161,247 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
return result;
}
protected ExtensionResourceMap registerExtensionWithPhysicalNetwork(PhysicalNetworkVO physicalNetwork,
Extension extension, Map<String, String> 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<ExtensionResourceMapVO> 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<String> services = resolveExtensionServices(extension);
return Transaction.execute((TransactionCallbackWithException<ExtensionResourceMap, CloudRuntimeException>) 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<ExtensionResourceMapDetailsVO> detailsVOList = new ArrayList<>();
if (MapUtils.isNotEmpty(details)) {
for (Map.Entry<String, String> 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 extensionMap;
});
}
/**
* 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<String> resolveExtensionServices(Extension extension) {
Map<String, String> extDetails = extensionDetailsDao.listDetailsKeyPairs(extension.getId());
Set<String> parsed = parseServicesFromDetailKeys(extDetails);
if (!parsed.isEmpty()) {
return parsed;
}
// Default: the full set of services NetworkExtensionElement supports
return new HashSet<>();
}
/**
* Resolves the set of service names from the extension detail map.
* From {@code network.services} comma-separated key.
*/
@SuppressWarnings("deprecation")
private Set<String> parseServicesFromDetailKeys(Map<String, String> 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<String> 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<Service, Map<Capability, String>>} from the
* extension detail map. From the split keys
* {@code network.services} + {@code network.service.capabilities}.
*/
@SuppressWarnings("deprecation")
private Map<Service, Map<Capability, String>> buildCapabilitiesFromDetailKeys(
Map<String, String> extDetails) {
if (extDetails == null) {
return new HashMap<>();
}
// New split format
if (extDetails.containsKey(ExtensionHelper.NETWORK_SERVICES_DETAIL_KEY)) {
Set<String> serviceNames = parseServicesFromDetailKeys(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<Service, Map<Capability, String>> 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<Capability, String> capMap = new HashMap<>();
if (capsObj != null && capsObj.has(svcName)) {
JsonObject svcCaps = capsObj.getAsJsonObject(svcName);
for (Map.Entry<String, JsonElement> 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<String> 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"));
}
/** Keys that are always stored with display=false (sensitive). */
private static final Set<String> 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<String, String> extDetails = extensionDetailsDao.listDetailsKeyPairs(extension.getId());
Set<String> allowedServices = parseServicesFromDetailKeys(extDetails);
if (allowedServices.isEmpty()) {
// No services declared accept any
return;
}
// Parse the requested services: either comma-separated string or JSON array
List<String> requested = parseServicesList(servicesValue);
List<String> 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<String> parseServicesList(String value) {
if (StringUtils.isBlank(value)) {
return Collections.emptyList();
}
value = value.trim();
if (value.startsWith("[")) {
try {
JsonArray arr = JsonParser.parseString(value).getAsJsonArray();
List<String> 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 +1410,15 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
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 +1438,55 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
}
}
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<ExtensionResourceMapVO> 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<NetworkVO> 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(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<ApiConstants.ExtensionDetails> viewDetails) {
@ -988,6 +1510,12 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
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<String, String> details = extensionResourceMapDetailsDao.listDetailsKeyPairs(
extensionResourceMapVO.getId(), true);
@ -1423,6 +1951,10 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
Pair<Long, Long> 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);
}
if (clusterId == null || hostId == null) {
@ -1499,6 +2031,92 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
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<String, String> cmdParameters) {
final String actionName = customActionVO.getName();
CustomActionResultResponse response = new CustomActionResultResponse();
response.setId(customActionVO.getUuid());
response.setName(actionName);
response.setObjectName("customactionresult");
Map<String, String> result = new HashMap<>();
response.setSuccess(false);
result.put(ApiConstants.MESSAGE, getActionMessage(false, customActionVO, extensionVO, actionResourceType, network));
// Resolve action parameters
List<ExtensionCustomAction.Parameter> actionParameters = null;
Pair<Map<String, String>, Map<String, String>> allDetails =
extensionCustomActionDetailsDao.listDetailsKeyPairsWithVisibility(customActionVO.getId());
if (allDetails.second().containsKey(ApiConstants.PARAMETERS)) {
actionParameters = ExtensionCustomAction.Parameter.toListFromJson(
allDetails.second().get(ApiConstants.PARAMETERS));
}
Map<String, Object> parameters = null;
if (CollectionUtils.isNotEmpty(actionParameters)) {
parameters = ExtensionCustomAction.Parameter.validateParameterValues(actionParameters, cmdParameters);
}
// Find the provider name for this network (try each service until we find one)
String providerName = null;
for (Service service : new Service[]{Service.SourceNat, Service.StaticNat,
Service.PortForwarding, Service.Firewall, Service.Gateway}) {
providerName = networkServiceMapDao.getProviderForServiceInNetwork(network.getId(), service);
if (StringUtils.isNotBlank(providerName)) {
break;
}
}
if (StringUtils.isBlank(providerName)) {
logger.error("No network service provider found for network {}", network.getId());
result.put(ApiConstants.DETAILS, "No network service provider found for this network");
response.setResult(result);
return response;
}
// Get the network element implementing that provider
NetworkElement element = networkModel.getElementImplementingProvider(providerName);
if (element == null) {
logger.error("No NetworkElement found implementing provider '{}' for network {}", providerName, network.getId());
result.put(ApiConstants.DETAILS, "No network element found for provider: " + providerName);
response.setResult(result);
return response;
}
// The element must implement NetworkCustomActionProvider
if (!(element instanceof NetworkCustomActionProvider)) {
logger.error("Network element '{}' for provider '{}' does not support custom actions",
element.getClass().getSimpleName(), providerName);
result.put(ApiConstants.DETAILS, "Provider '" + providerName + "' does not support custom actions");
response.setResult(result);
return response;
}
NetworkCustomActionProvider provider = (NetworkCustomActionProvider) element;
try {
if (!provider.canHandleCustomAction(network)) {
throw new CloudRuntimeException("Provider '" + providerName + "' cannot handle custom action for this network");
}
logger.info("Running network custom action '{}' on network {} via {} (provider: {})",
actionName, network.getId(), element.getClass().getSimpleName(), providerName);
String output = provider.runCustomAction(network, actionName, parameters);
boolean success = output != null;
response.setSuccess(success);
result.put(ApiConstants.MESSAGE, getActionMessage(success, customActionVO, extensionVO, actionResourceType, network));
result.put(ApiConstants.DETAILS, success ? output : "Action failed — check management server logs for details");
} catch (Exception e) {
logger.error("Network custom action '{}' threw exception: {}", actionName, e.getMessage(), e);
result.put(ApiConstants.DETAILS, "Action failed: " + e.getMessage());
}
response.setResult(result);
return response;
}
@Override
public ExtensionCustomActionResponse createCustomActionResponse(ExtensionCustomAction customAction) {
ExtensionCustomActionResponse response = new ExtensionCustomActionResponse(customAction.getUuid(),
@ -1608,11 +2226,8 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
if (MapUtils.isEmpty(details)) {
return;
}
List<ExtensionResourceMapDetailsVO> detailsList = new ArrayList<>();
for (Map.Entry<String, String> entry : details.entrySet()) {
detailsList.add(new ExtensionResourceMapDetailsVO(extensionResourceMapId, entry.getKey(),
entry.getValue()));
}
List<ExtensionResourceMapDetailsVO> detailsList =
buildExtensionResourceDetailsArray(extensionResourceMapId, details);
extensionResourceMapDetailsDao.saveDetails(detailsList);
}
@ -1677,6 +2292,26 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
return reservedDetails;
}
@Override
public Long getExtensionIdForPhysicalNetwork(long physicalNetworkId) {
// Returns the first (primary) extension for backward compatibility
List<ExtensionResourceMapVO> maps = extensionResourceMapDao.listByResourceIdAndType(physicalNetworkId,
ExtensionResourceMap.ResourceType.PhysicalNetwork);
if (maps == null || maps.isEmpty()) {
return null;
}
return maps.get(0).getExtensionId();
}
@Override
public Extension getExtensionForPhysicalNetwork(long physicalNetworkId) {
Long extensionId = getExtensionIdForPhysicalNetwork(physicalNetworkId);
if (extensionId == null) {
return null;
}
return extensionDao.findById(extensionId);
}
@Override
public boolean start() {
long pathStateCheckInterval = PathStateCheckInterval.value();
@ -1714,6 +2349,7 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
cmds.add(UpdateExtensionCmd.class);
cmds.add(RegisterExtensionCmd.class);
cmds.add(UnregisterExtensionCmd.class);
cmds.add(UpdateRegisteredExtensionCmd.class);
return cmds;
}
@ -1765,4 +2401,106 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
}
}
}
@Override
public String getExtensionScriptPath(Extension extension) {
if (extension == null) {
return null;
}
return externalProvisioner.getExtensionPath(extension.getRelativePath());
}
@Override
public Map<String, String> getExtensionDetails(long extensionId) {
return extensionDetailsDao.listDetailsKeyPairs(extensionId);
}
@Override
public Extension getExtensionForPhysicalNetworkAndProvider(long physicalNetworkId, String providerName) {
if (StringUtils.isBlank(providerName)) {
return null;
}
List<ExtensionResourceMapVO> maps = extensionResourceMapDao.listByResourceIdAndType(
physicalNetworkId, ExtensionResourceMap.ResourceType.PhysicalNetwork);
if (maps == null || maps.isEmpty()) {
return null;
}
for (ExtensionResourceMapVO map : maps) {
ExtensionVO ext = extensionDao.findById(map.getExtensionId());
if (ext != null && providerName.equalsIgnoreCase(ext.getName())) {
return ext;
}
}
return null;
}
@Override
public Map<String, String> getAllResourceMapDetailsForExtensionOnPhysicalNetwork(long physicalNetworkId, long extensionId) {
List<ExtensionResourceMapVO> maps = extensionResourceMapDao.listByResourceIdAndType(
physicalNetworkId, ExtensionResourceMap.ResourceType.PhysicalNetwork);
if (maps == null || maps.isEmpty()) {
return new HashMap<>();
}
for (ExtensionResourceMapVO map : maps) {
if (map.getExtensionId() == extensionId) {
Map<String, String> details = extensionResourceMapDetailsDao.listDetailsKeyPairs(map.getId());
return details != null ? details : new HashMap<>();
}
}
return new HashMap<>();
}
@Override
public boolean isNetworkExtensionProvider(String providerName) {
if (StringUtils.isBlank(providerName)) {
return false;
}
List<ExtensionVO> networkOrchExtensions = extensionDao.listByType(Extension.Type.NetworkOrchestrator);
if (networkOrchExtensions == null || networkOrchExtensions.isEmpty()) {
return false;
}
return networkOrchExtensions.stream()
.anyMatch(ext -> providerName.equalsIgnoreCase(ext.getName()));
}
@Override
public List<Extension> listExtensionsByType(Extension.Type type) {
if (type == null) {
return new ArrayList<>();
}
List<ExtensionVO> extensions = extensionDao.listByType(type);
if (extensions == null || extensions.isEmpty()) {
return new ArrayList<>();
}
return new ArrayList<>(extensions);
}
@Override
public Map<Service, Map<Capability, String>> getNetworkCapabilitiesForProvider(Long physicalNetworkId,
String providerName) {
if (StringUtils.isBlank(providerName)) {
return new HashMap<>();
}
Extension extension = null;
if (physicalNetworkId != null) {
extension = getExtensionForPhysicalNetworkAndProvider(physicalNetworkId, providerName);
}
if (extension == null) {
// Search across all physical networks
List<ExtensionVO> networkOrchExtensions = extensionDao.listByType(Extension.Type.NetworkOrchestrator);
if (networkOrchExtensions != null) {
for (ExtensionVO ext : networkOrchExtensions) {
if (providerName.equalsIgnoreCase(ext.getName())) {
extension = ext;
break;
}
}
}
}
if (extension == null) {
return new HashMap<>();
}
Map<String, String> extDetails = extensionDetailsDao.listDetailsKeyPairs(extension.getId());
return buildCapabilitiesFromDetailKeys(extDetails);
}
}

View File

@ -40,7 +40,7 @@ public class ExtensionDetailsVO implements ResourceDetail {
@Column(name = "name", nullable = false, length = 255)
private String name;
@Column(name = "value", nullable = false, length = 255)
@Column(name = "value", nullable = false, length = 4096)
private String value;
@Column(name = "display")

View File

@ -40,7 +40,7 @@ public class ExtensionResourceMapDetailsVO implements ResourceDetail {
@Column(name = "name", nullable = false, length = 255)
private String name;
@Column(name = "value", nullable = false, length = 255)
@Column(name = "value", nullable = false, length = 4096)
private String value;
@Column(name = "display")

View File

@ -33,4 +33,7 @@
<bean id="ExtensionsManager" class="org.apache.cloudstack.framework.extensions.manager.ExtensionsManagerImpl" />
<bean id="ExtensionCustomActionDaoImpl" class="org.apache.cloudstack.framework.extensions.dao.ExtensionCustomActionDaoImpl" />
<bean id="ExtensionCustomActionDetailsDaoImpl" class="org.apache.cloudstack.framework.extensions.dao.ExtensionCustomActionDetailsDaoImpl" />
<bean id="networkExtension" class="org.apache.cloudstack.framework.extensions.network.NetworkExtensionElement">
<property name="name" value="NetworkExtension" />
</bean>
</beans>

View File

@ -0,0 +1,115 @@
// 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 static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.EnumSet;
import java.util.Map;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.ApiErrorCode;
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 org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.test.util.ReflectionTestUtils;
public class UpdateRegisteredExtensionCmdTest {
private UpdateRegisteredExtensionCmd cmd;
private ExtensionsManager extensionsManager;
@Before
public void setUp() {
cmd = Mockito.spy(new UpdateRegisteredExtensionCmd());
extensionsManager = mock(ExtensionsManager.class);
ReflectionTestUtils.setField(cmd, "extensionsManager", extensionsManager);
}
@Test
public void extensionIdReturnsNullWhenUnset() {
ReflectionTestUtils.setField(cmd, "extensionId", null);
assertNull(cmd.getExtensionId());
}
@Test
public void resourceIdReturnsValueWhenSet() {
String resourceId = "resource-123";
ReflectionTestUtils.setField(cmd, "resourceId", resourceId);
assertEquals(resourceId, cmd.getResourceId());
}
@Test
public void resourceTypeReturnsValueWhenSet() {
String resourceType = "PhysicalNetwork";
ReflectionTestUtils.setField(cmd, "resourceType", resourceType);
assertEquals(resourceType, cmd.getResourceType());
}
@Test
public void detailsReturnsEmptyMapWhenUnset() {
ReflectionTestUtils.setField(cmd, "details", null);
Map<String, String> details = cmd.getDetails();
assertNotNull(details);
assertTrue(details.isEmpty());
}
@Test
public void executeSetsExtensionResponseWhenManagerSucceeds() {
Extension extension = mock(Extension.class);
ExtensionResponse response = mock(ExtensionResponse.class);
when(extensionsManager.updateRegisteredExtensionWithResource(cmd)).thenReturn(extension);
when(extensionsManager.createExtensionResponse(extension, EnumSet.of(ApiConstants.ExtensionDetails.all)))
.thenReturn(response);
doNothing().when(cmd).setResponseObject(any());
cmd.execute();
verify(extensionsManager).updateRegisteredExtensionWithResource(cmd);
verify(extensionsManager).createExtensionResponse(extension, EnumSet.of(ApiConstants.ExtensionDetails.all));
verify(cmd).setResponseObject(response);
}
@Test
public void executeThrowsServerApiExceptionWhenManagerFails() {
when(extensionsManager.updateRegisteredExtensionWithResource(cmd))
.thenThrow(new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to update registered extension"));
try {
cmd.execute();
fail("Expected ServerApiException");
} catch (ServerApiException e) {
assertEquals("Failed to update registered extension", e.getDescription());
}
}
}

View File

@ -37,6 +37,7 @@ import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ -74,6 +75,7 @@ import org.apache.cloudstack.framework.extensions.api.RunCustomActionCmd;
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.ExtensionServerActionBaseCommand;
import org.apache.cloudstack.framework.extensions.command.GetExtensionPathChecksumCommand;
@ -87,6 +89,7 @@ import org.apache.cloudstack.framework.extensions.dao.ExtensionResourceMapDetail
import org.apache.cloudstack.framework.extensions.vo.ExtensionCustomActionDetailsVO;
import org.apache.cloudstack.framework.extensions.vo.ExtensionCustomActionVO;
import org.apache.cloudstack.framework.extensions.vo.ExtensionDetailsVO;
import org.apache.cloudstack.framework.extensions.vo.ExtensionResourceMapDetailsVO;
import org.apache.cloudstack.framework.extensions.vo.ExtensionResourceMapVO;
import org.apache.cloudstack.framework.extensions.vo.ExtensionVO;
import org.apache.cloudstack.utils.identity.ManagementServerNode;
@ -94,6 +97,7 @@ import org.apache.commons.collections.CollectionUtils;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
@ -122,6 +126,15 @@ import com.cloud.host.dao.HostDao;
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.NetworkModel;
import com.cloud.network.dao.NetworkDao;
import com.cloud.network.dao.NetworkServiceMapDao;
import com.cloud.network.dao.NetworkVO;
import com.cloud.network.dao.PhysicalNetworkDao;
import com.cloud.network.dao.PhysicalNetworkVO;
import com.cloud.network.element.NetworkElement;
import org.apache.cloudstack.extension.NetworkCustomActionProvider;
import com.cloud.org.Cluster;
import com.cloud.serializer.GsonHelper;
import com.cloud.storage.dao.VMTemplateDao;
@ -138,7 +151,7 @@ import com.cloud.vm.VirtualMachineManager;
import com.cloud.vm.VmDetailConstants;
import com.cloud.vm.dao.VMInstanceDao;
@RunWith(MockitoJUnitRunner.class)
@RunWith(MockitoJUnitRunner.Silent.class)
public class ExtensionsManagerImplTest {
@Spy
@ -185,6 +198,14 @@ public class ExtensionsManagerImplTest {
private RoleService roleService;
@Mock
private AccountService accountService;
@Mock
private PhysicalNetworkDao physicalNetworkDao;
@Mock
private NetworkDao networkDao;
@Mock
private NetworkServiceMapDao networkServiceMapDao;
@Mock
private NetworkModel networkModel;
@Before
public void setUp() {
@ -290,14 +311,6 @@ public class ExtensionsManagerImplTest {
assertNull(extensionsManager.getExtensionFromResource(ExtensionCustomAction.ResourceType.VirtualMachine, "uuid"));
}
@Test
public void getActionMessageReturnsDefaultOnBlank() {
ExtensionCustomAction action = mock(ExtensionCustomAction.class);
Extension ext = mock(Extension.class);
when(action.getSuccessMessage()).thenReturn(null);
String msg = extensionsManager.getActionMessage(true, action, ext, ExtensionCustomAction.ResourceType.VirtualMachine, null);
assertTrue(msg.contains("Successfully completed"));
}
@Test
public void getActionMessageReturnsDefaultMessageForSuccessWithoutCustomMessage() {
@ -343,14 +356,6 @@ public class ExtensionsManagerImplTest {
assertEquals("Custom failure message", result);
}
@Test
public void getActionMessageHandlesNullActionMessage() {
ExtensionCustomAction action = mock(ExtensionCustomAction.class);
when(action.getSuccessMessage()).thenReturn(null);
Extension extension = mock(Extension.class);
String result = extensionsManager.getActionMessage(true, action, extension, ExtensionCustomAction.ResourceType.VirtualMachine, null);
assertTrue(result.contains("Successfully completed"));
}
@Test
public void getFilteredExternalDetailsReturnsFilteredMap() {
@ -381,26 +386,6 @@ public class ExtensionsManagerImplTest {
anyLong(), anyLong(), anyString(), anyString());
}
@Test
public void updateExtensionPathReadyUpdatesWhenStateDiffers() {
Extension ext = mock(Extension.class);
when(ext.getId()).thenReturn(1L);
when(ext.isPathReady()).thenReturn(false);
ExtensionVO vo = mock(ExtensionVO.class);
when(extensionDao.createForUpdate(1L)).thenReturn(vo);
when(extensionDao.update(1L, vo)).thenReturn(true);
extensionsManager.updateExtensionPathReady(ext, true);
verify(extensionDao).update(1L, vo);
}
@Test
public void disableExtensionUpdatesState() {
ExtensionVO vo = mock(ExtensionVO.class);
when(extensionDao.createForUpdate(1L)).thenReturn(vo);
when(extensionDao.update(1L, vo)).thenReturn(true);
extensionsManager.disableExtension(1L);
verify(extensionDao).update(1L, vo);
}
@Test
public void getExtensionFromResourceReturnsExtensionForValidResource() {
@ -558,41 +543,29 @@ public class ExtensionsManagerImplTest {
assertEquals("/tmp/extensions", extensionsManager.getExtensionsPath());
}
@Test
public void getExtensionIdForClusterReturnsNullIfNoMap() {
when(extensionResourceMapDao.findByResourceIdAndType(anyLong(), any())).thenReturn(null);
assertNull(extensionsManager.getExtensionIdForCluster(1L));
public void checkExtensionPathSyncUpdatesReadyWhenStateDiffers() {
Extension ext = mock(Extension.class);
when(ext.getName()).thenReturn("ext");
when(ext.getRelativePath()).thenReturn("entry.sh");
when(ext.isPathReady()).thenReturn(false);
ExtensionVO vo = mock(ExtensionVO.class);
when(extensionDao.createForUpdate(1L)).thenReturn(vo);
when(extensionDao.update(1L, vo)).thenReturn(true);
extensionsManager.checkExtensionPathState(ext, Collections.emptyList());
verify(extensionsManager).updateExtensionPathReady(ext, false);
}
@Test
public void getExtensionIdForClusterReturnsIdIfMapExists() {
ExtensionResourceMapVO map = mock(ExtensionResourceMapVO.class);
when(map.getExtensionId()).thenReturn(5L);
when(extensionResourceMapDao.findByResourceIdAndType(anyLong(), any())).thenReturn(map);
assertEquals(Long.valueOf(5L), extensionsManager.getExtensionIdForCluster(1L));
}
@Test
public void getExtensionReturnsExtension() {
ExtensionVO ext = mock(ExtensionVO.class);
when(extensionDao.findById(1L)).thenReturn(ext);
assertEquals(ext, extensionsManager.getExtension(1L));
}
@Test
public void getExtensionForClusterReturnsNullIfNoId() {
when(extensionResourceMapDao.findByResourceIdAndType(anyLong(), any())).thenReturn(null);
assertNull(extensionsManager.getExtensionForCluster(1L));
}
@Test
public void getExtensionForClusterReturnsExtensionIfIdExists() {
ExtensionResourceMapVO map = mock(ExtensionResourceMapVO.class);
when(map.getExtensionId()).thenReturn(5L);
when(extensionResourceMapDao.findByResourceIdAndType(anyLong(), any())).thenReturn(map);
ExtensionVO ext = mock(ExtensionVO.class);
when(extensionDao.findById(5L)).thenReturn(ext);
assertEquals(ext, extensionsManager.getExtensionForCluster(1L));
public void checkExtensionPathSyncUpdatesReadyWhenStateUnchanged() {
Extension ext = mock(Extension.class);
when(ext.getName()).thenReturn("ext");
when(ext.getRelativePath()).thenReturn("entry.sh");
when(ext.isPathReady()).thenReturn(true);
when(externalProvisioner.getChecksumForExtensionPath("ext", "entry.sh")).thenReturn("checksum123");
extensionsManager.checkExtensionPathState(ext, Collections.emptyList());
verify(extensionsManager, times(1)).updateExtensionPathReady(any(), anyBoolean());
}
@Test
@ -1024,13 +997,6 @@ public class ExtensionsManagerImplTest {
verify(extensionDao).remove(1L);
}
@Test
public void testRegisterExtensionWithResource_InvalidResourceType() {
RegisterExtensionCmd cmd = mock(RegisterExtensionCmd.class);
when(cmd.getResourceType()).thenReturn("InvalidType");
assertThrows(InvalidParameterValueException.class, () -> extensionsManager.registerExtensionWithResource(cmd));
}
@Test
public void registerExtensionWithResourceRegistersSuccessfullyForValidResourceType() {
@ -1063,8 +1029,6 @@ public class ExtensionsManagerImplTest {
RegisterExtensionCmd cmd = mock(RegisterExtensionCmd.class);
when(cmd.getResourceType()).thenReturn(ExtensionResourceMap.ResourceType.Cluster.name());
when(cmd.getResourceId()).thenReturn(UUID.randomUUID().toString());
ClusterVO clusterVO = mock(ClusterVO.class);
when(clusterDao.findByUuid(anyString())).thenReturn(clusterVO);
extensionsManager.registerExtensionWithResource(cmd);
}
@ -1139,6 +1103,159 @@ public class ExtensionsManagerImplTest {
verify(extensionResourceMapDao, never()).remove(anyLong());
}
@Test
public void unregisterExtensionWithResourceThrowsWhenProviderUsedByExistingNetworks() {
UnregisterExtensionCmd cmd = mock(UnregisterExtensionCmd.class);
when(cmd.getResourceType()).thenReturn(ExtensionResourceMap.ResourceType.PhysicalNetwork.name());
when(cmd.getResourceId()).thenReturn("physnet-uuid");
when(cmd.getExtensionId()).thenReturn(1L);
PhysicalNetworkVO physicalNetwork = mock(PhysicalNetworkVO.class);
when(physicalNetwork.getId()).thenReturn(42L);
when(physicalNetworkDao.findByUuid("physnet-uuid")).thenReturn(physicalNetwork);
ExtensionResourceMapVO existing = mock(ExtensionResourceMapVO.class);
when(existing.getExtensionId()).thenReturn(1L);
when(extensionResourceMapDao.listByResourceIdAndType(42L, ExtensionResourceMap.ResourceType.PhysicalNetwork))
.thenReturn(List.of(existing));
ExtensionVO extension = mock(ExtensionVO.class);
when(extension.getName()).thenReturn("extnet-provider");
when(extensionDao.findById(1L)).thenReturn(extension);
NetworkVO network = mock(NetworkVO.class);
when(networkDao.listByPhysicalNetworkAndProvider(42L, "extnet-provider")).thenReturn(List.of(network));
assertThrows(CloudRuntimeException.class, () -> extensionsManager.unregisterExtensionWithResource(cmd));
verify(extensionResourceMapDao, never()).remove(anyLong());
}
@Test
public void updateRegisteredExtensionWithResourceUpdatesDetailsForExistingMapping() {
UpdateRegisteredExtensionCmd cmd = mock(UpdateRegisteredExtensionCmd.class);
when(cmd.getResourceType()).thenReturn(ExtensionResourceMap.ResourceType.PhysicalNetwork.name());
when(cmd.getResourceId()).thenReturn("physnet-uuid");
when(cmd.getExtensionId()).thenReturn(1L);
when(cmd.getDetails()).thenReturn(Map.of("username", "root", "hosts", "10.10.10.10"));
when(cmd.isCleanupDetails()).thenReturn(false);
ExtensionVO extension = mock(ExtensionVO.class);
when(extension.getName()).thenReturn("extnet-provider");
when(extensionDao.findById(1L)).thenReturn(extension);
PhysicalNetworkVO physicalNetwork = mock(PhysicalNetworkVO.class);
when(physicalNetwork.getId()).thenReturn(42L);
when(physicalNetworkDao.findByUuid("physnet-uuid")).thenReturn(physicalNetwork);
ExtensionResourceMapVO existing = mock(ExtensionResourceMapVO.class);
when(existing.getExtensionId()).thenReturn(1L);
when(existing.getId()).thenReturn(100L);
when(extensionResourceMapDao.listByResourceIdAndType(42L, ExtensionResourceMap.ResourceType.PhysicalNetwork))
.thenReturn(List.of(existing));
Extension result = extensionsManager.updateRegisteredExtensionWithResource(cmd);
assertEquals(extension, result);
verify(extensionResourceMapDetailsDao, never()).removeDetails(anyLong());
verify(extensionResourceMapDetailsDao).saveDetails(any());
}
@Test
public void updateRegisteredExtensionWithResourceCleanupDetailsFirstThenSaveRequested() {
UpdateRegisteredExtensionCmd cmd = mock(UpdateRegisteredExtensionCmd.class);
when(cmd.getResourceType()).thenReturn(ExtensionResourceMap.ResourceType.PhysicalNetwork.name());
when(cmd.getResourceId()).thenReturn("physnet-uuid");
when(cmd.getExtensionId()).thenReturn(1L);
when(cmd.getDetails()).thenReturn(Map.of("username", "root", "password", "secret"));
when(cmd.isCleanupDetails()).thenReturn(true);
ExtensionVO extension = mock(ExtensionVO.class);
when(extensionDao.findById(1L)).thenReturn(extension);
PhysicalNetworkVO physicalNetwork = mock(PhysicalNetworkVO.class);
when(physicalNetwork.getId()).thenReturn(42L);
when(physicalNetworkDao.findByUuid("physnet-uuid")).thenReturn(physicalNetwork);
ExtensionResourceMapVO existing = mock(ExtensionResourceMapVO.class);
when(existing.getExtensionId()).thenReturn(1L);
when(existing.getId()).thenReturn(100L);
when(extensionResourceMapDao.listByResourceIdAndType(42L, ExtensionResourceMap.ResourceType.PhysicalNetwork))
.thenReturn(List.of(existing));
extensionsManager.updateRegisteredExtensionWithResource(cmd);
verify(extensionResourceMapDetailsDao).removeDetails(100L);
verify(extensionResourceMapDetailsDao, never()).saveDetails(any());
}
@Test
@SuppressWarnings("unchecked")
public void updateRegisteredExtensionWithResourceStoresSensitiveDetailsWithDisplayFalse() {
UpdateRegisteredExtensionCmd cmd = mock(UpdateRegisteredExtensionCmd.class);
when(cmd.getResourceType()).thenReturn(ExtensionResourceMap.ResourceType.PhysicalNetwork.name());
when(cmd.getResourceId()).thenReturn("physnet-uuid");
when(cmd.getExtensionId()).thenReturn(1L);
when(cmd.getDetails()).thenReturn(Map.of("username", "root", "password", "newSecret"));
when(cmd.isCleanupDetails()).thenReturn(false);
ExtensionVO extension = mock(ExtensionVO.class);
when(extensionDao.findById(1L)).thenReturn(extension);
PhysicalNetworkVO physicalNetwork = mock(PhysicalNetworkVO.class);
when(physicalNetwork.getId()).thenReturn(42L);
when(physicalNetworkDao.findByUuid("physnet-uuid")).thenReturn(physicalNetwork);
ExtensionResourceMapVO existing = mock(ExtensionResourceMapVO.class);
when(existing.getExtensionId()).thenReturn(1L);
when(existing.getId()).thenReturn(100L);
when(extensionResourceMapDao.listByResourceIdAndType(42L, ExtensionResourceMap.ResourceType.PhysicalNetwork))
.thenReturn(List.of(existing));
extensionsManager.updateRegisteredExtensionWithResource(cmd);
ArgumentCaptor<List<ExtensionResourceMapDetailsVO>> captor = ArgumentCaptor.forClass(List.class);
verify(extensionResourceMapDetailsDao).saveDetails(captor.capture());
verify(extensionResourceMapDetailsDao, never()).removeDetails(anyLong());
List<ExtensionResourceMapDetailsVO> savedDetails = captor.getValue();
ExtensionResourceMapDetailsVO passwordDetail = savedDetails.stream()
.filter(detail -> "password".equals(detail.getName()))
.findFirst()
.orElse(null);
assertNotNull(passwordDetail);
assertFalse(passwordDetail.isDisplay());
assertEquals("newSecret", passwordDetail.getValue());
}
@Test
@SuppressWarnings("unchecked")
public void registerExtensionWithClusterStoresSensitiveDetailsWithDisplayFalse() {
Cluster cluster = mock(Cluster.class);
when(cluster.getId()).thenReturn(12L);
when(cluster.getName()).thenReturn("cluster-12");
when(cluster.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.External);
Extension extension = mock(Extension.class);
when(extension.getId()).thenReturn(5L);
ExtensionResourceMapVO persistedMap = mock(ExtensionResourceMapVO.class);
when(persistedMap.getId()).thenReturn(120L);
when(extensionResourceMapDao.persist(any())).thenReturn(persistedMap);
extensionsManager.registerExtensionWithCluster(cluster, extension,
Map.of("username", "admin", "password", "s3cr3t"));
ArgumentCaptor<List<ExtensionResourceMapDetailsVO>> captor = ArgumentCaptor.forClass(List.class);
verify(extensionResourceMapDetailsDao).saveDetails(captor.capture());
List<ExtensionResourceMapDetailsVO> savedDetails = captor.getValue();
ExtensionResourceMapDetailsVO passwordDetail = savedDetails.stream()
.filter(detail -> "password".equals(detail.getName()))
.findFirst()
.orElse(null);
assertNotNull(passwordDetail);
assertFalse(passwordDetail.isDisplay());
}
@Test
public void testCreateExtensionResponse_BasicFields() {
Extension extension = mock(Extension.class);
@ -2218,4 +2335,142 @@ public class ExtensionsManagerImplTest {
assertEquals(reservedResourceDetails.size(), entry.getValue().size());
assertTrue(reservedResourceDetails.containsAll(entry.getValue()));
}
// -----------------------------------------------------------------------
// Tests for ExtensionHelper methods (external network device support)
// -----------------------------------------------------------------------
@Test
public void getExtensionForPhysicalNetworkReturnsExtensionWhenRegistered() {
long physNetId = 10L;
long extensionId = 5L;
ExtensionResourceMapVO mapVO = mock(ExtensionResourceMapVO.class);
when(mapVO.getExtensionId()).thenReturn(extensionId);
when(extensionResourceMapDao.listByResourceIdAndType(physNetId,
ExtensionResourceMap.ResourceType.PhysicalNetwork)).thenReturn(List.of(mapVO));
ExtensionVO ext = mock(ExtensionVO.class);
when(extensionDao.findById(extensionId)).thenReturn(ext);
Extension result = extensionsManager.getExtensionForPhysicalNetwork(physNetId);
assertNotNull(result);
assertEquals(ext, result);
}
@Test
public void getExtensionForPhysicalNetworkReturnsNullWhenNotRegistered() {
long physNetId = 10L;
when(extensionResourceMapDao.listByResourceIdAndType(physNetId,
ExtensionResourceMap.ResourceType.PhysicalNetwork)).thenReturn(Collections.emptyList());
Extension result = extensionsManager.getExtensionForPhysicalNetwork(physNetId);
assertNull(result);
}
// Helper: a mock object that is both a NetworkElement and a NetworkCustomActionProvider
interface MockNetworkElement extends NetworkElement, NetworkCustomActionProvider {}
@Test
public void runNetworkCustomActionSucceeds() {
Network network = mock(Network.class);
when(network.getId()).thenReturn(5L);
when(network.getName()).thenReturn("test-net");
ExtensionCustomActionVO actionVO = mock(ExtensionCustomActionVO.class);
when(actionVO.getUuid()).thenReturn("action-uuid");
when(actionVO.getName()).thenReturn("reboot-device");
when(actionVO.getId()).thenReturn(1L);
when(actionVO.getSuccessMessage()).thenReturn(null);
when(actionVO.getErrorMessage()).thenReturn(null);
ExtensionVO extensionVO = mock(ExtensionVO.class);
when(extensionVO.getName()).thenReturn("my-extnet");
Pair<Map<String, String>, Map<String, String>> details = new Pair<>(new HashMap<>(), new HashMap<>());
when(extensionCustomActionDetailsDao.listDetailsKeyPairsWithVisibility(1L)).thenReturn(details);
// networkServiceMapDao returns provider name for SourceNat
when(networkServiceMapDao.getProviderForServiceInNetwork(eq(5L), any())).thenReturn("my-extnet");
// element implements both NetworkElement and NetworkCustomActionProvider
MockNetworkElement element = mock(MockNetworkElement.class);
when(element.canHandleCustomAction(eq(network))).thenReturn(true);
when(element.runCustomAction(eq(network), eq("reboot-device"), any())).thenReturn("OK: bridge bounced");
when(networkModel.getElementImplementingProvider("my-extnet")).thenReturn(element);
CustomActionResultResponse resp = extensionsManager.runNetworkCustomAction(
network, actionVO, extensionVO,
ExtensionCustomAction.ResourceType.Network, new HashMap<>());
assertTrue(resp.isSuccess());
assertEquals("OK: bridge bounced", resp.getResult().get(ApiConstants.DETAILS));
}
@Test
public void runNetworkCustomActionFailsWhenNoProvider() {
Network network = mock(Network.class);
when(network.getId()).thenReturn(5L);
when(network.getName()).thenReturn("test-net");
ExtensionCustomActionVO actionVO = mock(ExtensionCustomActionVO.class);
when(actionVO.getUuid()).thenReturn("action-uuid");
when(actionVO.getName()).thenReturn("dump-config");
when(actionVO.getId()).thenReturn(2L);
when(actionVO.getSuccessMessage()).thenReturn(null);
when(actionVO.getErrorMessage()).thenReturn(null);
ExtensionVO extensionVO = mock(ExtensionVO.class);
when(extensionVO.getName()).thenReturn("my-extnet");
Pair<Map<String, String>, Map<String, String>> details = new Pair<>(new HashMap<>(), new HashMap<>());
when(extensionCustomActionDetailsDao.listDetailsKeyPairsWithVisibility(2L)).thenReturn(details);
// No provider found for any service
when(networkServiceMapDao.getProviderForServiceInNetwork(eq(5L), any())).thenReturn(null);
CustomActionResultResponse resp = extensionsManager.runNetworkCustomAction(
network, actionVO, extensionVO,
ExtensionCustomAction.ResourceType.Network, new HashMap<>());
assertFalse(resp.isSuccess());
assertTrue(resp.getResult().get(ApiConstants.DETAILS).contains("No network service provider"));
}
@Test
public void runNetworkCustomActionFailsWhenProviderReturnsNull() {
Network network = mock(Network.class);
when(network.getId()).thenReturn(5L);
when(network.getName()).thenReturn("test-net");
ExtensionCustomActionVO actionVO = mock(ExtensionCustomActionVO.class);
when(actionVO.getUuid()).thenReturn("action-uuid");
when(actionVO.getName()).thenReturn("unknown-action");
when(actionVO.getId()).thenReturn(3L);
when(actionVO.getSuccessMessage()).thenReturn(null);
when(actionVO.getErrorMessage()).thenReturn(null);
ExtensionVO extensionVO = mock(ExtensionVO.class);
when(extensionVO.getName()).thenReturn("my-extnet");
Pair<Map<String, String>, Map<String, String>> details = new Pair<>(new HashMap<>(), new HashMap<>());
when(extensionCustomActionDetailsDao.listDetailsKeyPairsWithVisibility(3L)).thenReturn(details);
// networkServiceMapDao returns provider name
when(networkServiceMapDao.getProviderForServiceInNetwork(eq(5L), any())).thenReturn("my-extnet");
// element implements both NetworkElement and NetworkCustomActionProvider but action returns null
MockNetworkElement element = mock(MockNetworkElement.class);
when(element.runCustomAction(eq(network), eq("unknown-action"), any())).thenReturn(null);
when(networkModel.getElementImplementingProvider("my-extnet")).thenReturn(element);
CustomActionResultResponse resp = extensionsManager.runNetworkCustomAction(
network, actionVO, extensionVO,
ExtensionCustomAction.ResourceType.Network, new HashMap<>());
assertFalse(resp.isSuccess());
assertTrue(resp.getResult().get(ApiConstants.DETAILS).contains("Action failed"));
}
}

View File

@ -328,7 +328,7 @@ public class ManagementServerMock {
}
}
if (_znet.getState() != PhysicalNetwork.State.Enabled) {
_znet = _networkService.updatePhysicalNetwork(_znet.getId(), null, null, null, PhysicalNetwork.State.Enabled.toString());
_znet = _networkService.updatePhysicalNetwork(_znet.getId(), null, null, null, PhysicalNetwork.State.Enabled.toString(), null);
}
// Ensure that the physical network supports Guest traffic.

View File

@ -1559,6 +1559,10 @@ public class ApiDBUtils {
return s_networkModel.canElementEnableIndividualServices(serviceProvider);
}
public static boolean canElementEnableIndividualServicesByName(String providerName) {
return s_networkModel.canElementEnableIndividualServicesByName(providerName);
}
public static Pair<Long, Boolean> getDomainNetworkDetails(long networkId) {
NetworkDomainVO map = s_networkDomainDao.getDomainNetworkMapByNetworkId(networkId);

View File

@ -548,6 +548,8 @@ public class ApiResponseHelper implements ResponseGenerator, ResourceIdSupport {
ResourceIconManager resourceIconManager;
@Inject
AsyncJobDao asyncJobDao;
@Inject
NetworkModel networkModel;
public static String getPrettyDomainPath(String path) {
if (path == null) {
@ -3307,7 +3309,7 @@ public class ApiResponseHelper implements ResponseGenerator, ResourceIdSupport {
}
response.setServices(services);
Provider serviceProvider = Provider.getProvider(result.getProviderName());
Provider serviceProvider = networkModel.resolveProvider(result.getProviderName());
boolean canEnableIndividualServices = ApiDBUtils.canElementEnableIndividualServices(serviceProvider);
response.setCanEnableIndividualServices(canEnableIndividualServices);

View File

@ -7268,7 +7268,7 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati
}
for (final String prvNameStr : svcPrv.get(serviceStr)) {
// check if provider is supported
final Network.Provider provider = Network.Provider.getProvider(prvNameStr);
final Network.Provider provider = _networkModel.resolveProvider(prvNameStr);
if (provider == null) {
throw new InvalidParameterValueException("Invalid service provider: " + prvNameStr);
}
@ -7954,7 +7954,7 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati
// 1) Vaidate the detail values - have to match the lb provider
// name
final String providerStr = details.get(detail);
if (Network.Provider.getProvider(providerStr) == null) {
if (_networkModel.resolveProvider(providerStr) == null) {
throw new InvalidParameterValueException("Invalid value " + providerStr + " for the detail " + detail);
}
if (serviceProviderMap.get(Service.Lb) != null) {

View File

@ -99,6 +99,8 @@ import com.cloud.network.element.IpDeployingRequester;
import com.cloud.network.element.NetworkElement;
import com.cloud.network.element.UserDataServiceProvider;
import com.cloud.network.router.VirtualRouter;
import org.apache.cloudstack.extension.ExtensionHelper;
import org.apache.cloudstack.framework.extensions.network.NetworkExtensionElement;
import com.cloud.network.rules.FirewallRule.Purpose;
import com.cloud.network.rules.FirewallRuleVO;
import com.cloud.network.rules.dao.PortForwardingRulesDao;
@ -234,6 +236,11 @@ public class NetworkModelImpl extends ManagerBase implements NetworkModel, Confi
private NetworkService _networkService;
@Inject
TungstenGuestNetworkIpAddressDao tungstenGuestNetworkIpAddressDao;
@Inject
ExtensionHelper extensionHelper;
@Inject
private NetworkExtensionElement networkExtensionElement;
private final HashMap<String, NetworkOfferingVO> _systemNetworks = new HashMap<String, NetworkOfferingVO>(5);
@ -262,15 +269,39 @@ public class NetworkModelImpl extends ManagerBase implements NetworkModel, Confi
public NetworkElement getElementImplementingProvider(String providerName) {
String elementName = s_providerToNetworkElementMap.get(providerName);
NetworkElement element = AdapterBase.getAdapterByName(networkElements, elementName);
if (element == null && extensionHelper.isNetworkExtensionProvider(providerName)) {
// Provider is an extension-backed external network provider.
// Initialize a copy of NetworkExtensionElement
if (networkExtensionElement != null) {
element = networkExtensionElement.withProviderName(providerName);
}
}
return element;
}
/**
* Returns the effective network capabilities for an extension-backed external
* network provider on the given physical network.
*
* @param physicalNetworkId physical network ID (may be null for offering-level queries)
* @param providerName provider / extension name
* @return per-provider capabilities, or empty map if not found
*/
protected Map<Service, Map<Capability, String>> getExternalProviderCapabilities(
Long physicalNetworkId, String providerName) {
return extensionHelper.getNetworkCapabilitiesForProvider(physicalNetworkId, providerName);
}
@Override
public List<Service> getElementServices(Provider provider) {
NetworkElement element = getElementImplementingProvider(provider.getName());
if (element == null) {
throw new InvalidParameterValueException("Unable to find the Network Element implementing the Service Provider '" + provider.getName() + "'");
}
if (extensionHelper.isNetworkExtensionProvider(provider.getName())) {
Map<Service, Map<Capability, String>> caps = getExternalProviderCapabilities(null, provider.getName());
return new ArrayList<Service>(caps.keySet());
}
return new ArrayList<Service>(element.getCapabilities().keySet());
}
@ -283,6 +314,24 @@ public class NetworkModelImpl extends ManagerBase implements NetworkModel, Confi
return element.canEnableIndividualServices();
}
@Override
public boolean canElementEnableIndividualServicesByName(String providerName) {
if (providerName == null) {
return false;
}
// Try resolve to enum Provider first
Provider provider = resolveProvider(providerName);
if (provider != null) {
try {
return canElementEnableIndividualServices(provider);
} catch (Exception e) {
logger.debug("canElementEnableIndividualServices failed for provider {}: {}", providerName, e.getMessage());
}
}
// Unknown provider: be conservative and return false
return false;
}
Set<Purpose> getPublicIpPurposeInRules(PublicIpAddress ip, boolean includeRevoked, boolean includingFirewall) {
Set<Purpose> result = new HashSet<Purpose>();
List<FirewallRuleVO> rules = null;
@ -435,8 +484,11 @@ public class NetworkModelImpl extends ManagerBase implements NetworkModel, Confi
if (providers == null) {
providers = new HashSet<Provider>();
}
providers.add(Provider.getProvider(nsm.getProvider()));
map.put(Service.getService(nsm.getService()), providers);
Provider provider = resolveProvider(nsm.getProvider());
if (provider != null) {
providers.add(provider);
map.put(Service.getService(nsm.getService()), providers);
}
}
return map;
}
@ -481,16 +533,45 @@ public class NetworkModelImpl extends ManagerBase implements NetworkModel, Confi
Map<Provider, Set<Service>> map = new HashMap<Provider, Set<Service>>();
List<NetworkServiceMapVO> nsms = _ntwkSrvcDao.getServicesInNetwork(networkId);
for (NetworkServiceMapVO nsm : nsms) {
Set<Service> services = map.get(Provider.getProvider(nsm.getProvider()));
Provider provider = resolveProvider(nsm.getProvider());
if (provider == null) {
continue;
}
Set<Service> services = map.get(provider);
if (services == null) {
services = new HashSet<Service>();
}
services.add(Service.getService(nsm.getService()));
map.put(Provider.getProvider(nsm.getProvider()), services);
map.put(provider, services);
}
return map;
}
/**
* Resolves a provider name to a {@link Provider} instance.
*
* <p>For well-known providers, returns the static constant from
* {@link Provider#getProvider(String)}. For dynamic NetworkOrchestrator
* extension providers (whose names are not in the static registry), returns
* a transient {@link Provider} with the given name so that the caller can
* still dispatch to the correct {@link NetworkExtensionElement} via
* {@link #getElementImplementingProvider(String)}.</p>
*
* @param providerName the provider name from {@code ntwk_service_map}
* @return a {@link Provider} instance, or {@code null} if not resolvable
*/
@Override
public Provider resolveProvider(String providerName) {
Provider provider = Provider.getProvider(providerName);
if (provider == null && extensionHelper.isNetworkExtensionProvider(providerName)) {
// Dynamic extension-backed provider: create a transient Provider that preserves
// the actual extension name. getElementImplementingProvider() handles this name
// by detecting it as an extension provider and returning NetworkExtensionElement.
provider = Provider.createTransientProvider(providerName);
}
return provider;
}
@Override
public Map<Provider, ArrayList<PublicIpAddress>> getProviderToIpList(Network network, Map<PublicIpAddress, Set<Service>> ipToServices) {
NetworkOffering offering = _networkOfferingDao.findById(network.getNetworkOfferingId());
@ -694,11 +775,21 @@ public class NetworkModelImpl extends ManagerBase implements NetworkModel, Confi
// list all services of this networkOffering
List<NetworkServiceMapVO> servicesMap = _ntwkSrvcDao.getServicesInNetwork(networkId);
// Resolve the physical network once for external provider lookups
NetworkVO network = _networksDao.findById(networkId);
Long physicalNetworkId = network != null ? network.getPhysicalNetworkId() : null;
for (NetworkServiceMapVO instance : servicesMap) {
Service service = Service.getService(instance.getService());
NetworkElement element = getElementImplementingProvider(instance.getProvider());
if (element != null) {
Map<Service, Map<Capability, String>> elementCapabilities = element.getCapabilities();
Map<Service, Map<Capability, String>> elementCapabilities;
if (extensionHelper.isNetworkExtensionProvider(instance.getProvider()) && physicalNetworkId != null) {
elementCapabilities = getExternalProviderCapabilities(physicalNetworkId, instance.getProvider());
} else {
elementCapabilities = element.getCapabilities();
}
if (elementCapabilities != null) {
networkCapabilities.put(service, elementCapabilities.get(service));
}
@ -724,10 +815,15 @@ public class NetworkModelImpl extends ManagerBase implements NetworkModel, Confi
NetworkElement element = getElementImplementingProvider(provider);
if (element != null) {
Map<Service, Map<Capability, String>> elementCapabilities = element.getCapabilities();
;
Map<Service, Map<Capability, String>> elementCapabilities;
if (extensionHelper.isNetworkExtensionProvider(provider)) {
elementCapabilities = getExternalProviderCapabilities(null, provider);
} else {
elementCapabilities = element.getCapabilities();
}
if (elementCapabilities == null || !elementCapabilities.containsKey(service)) {
// TBD: We should be sending providerId and not the offering object itself.
throw new UnsupportedServiceException("Service " + service.getName() + " is not supported by the element=" + element.getName() +
" implementing Provider=" + provider);
}
@ -758,12 +854,22 @@ public class NetworkModelImpl extends ManagerBase implements NetworkModel, Confi
// we have to calculate capabilities for all of them
String provider = providers.get(0);
// Check if this is an extension-backed external network provider first.
// These providers are not in the static s_providerToNetworkElementMap so
// we resolve their capabilities from the extension details directly.
if (extensionHelper.isNetworkExtensionProvider(provider)) {
Map<Service, Map<Capability, String>> extCaps = getExternalProviderCapabilities(null, provider);
if (extCaps != null && extCaps.containsKey(service)) {
return extCaps.get(service);
}
return serviceCapabilities;
}
// FIXME we return the capabilities of the first provider of the service - what if we have multiple providers
// for same Service?
NetworkElement element = getElementImplementingProvider(provider);
if (element != null) {
Map<Service, Map<Capability, String>> elementCapabilities = element.getCapabilities();
;
if (elementCapabilities == null || !elementCapabilities.containsKey(service)) {
// TBD: We should be sending providerId and not the offering object itself.
@ -979,7 +1085,6 @@ public class NetworkModelImpl extends ManagerBase implements NetworkModel, Confi
return false;
}
@Override
public boolean areServicesSupportedByNetworkOffering(long networkOfferingId, Service... services) {
return (_ntwkOfferingSrvcDao.areServicesSupportedByNetworkOffering(networkOfferingId, services));
@ -1161,7 +1266,7 @@ public class NetworkModelImpl extends ManagerBase implements NetworkModel, Confi
if (providers == null) {
providers = new HashSet<Provider>();
}
providers.add(Provider.getProvider(instance.getProvider()));
providers.add(resolveProvider(instance.getProvider()));
serviceProviderMap.put(Service.getService(service), providers);
}
@ -1201,9 +1306,76 @@ public class NetworkModelImpl extends ManagerBase implements NetworkModel, Confi
}
}
// Also include extension-backed NetworkExtension providers registered in
// physical_network_service_providers whose provider name matches a registered
// NetworkOrchestrator extension (detected via extensionHelper.isNetworkExtensionProvider).
//
// We use _pNSPDao.listBy(physNetId) to enumerate all NSP entries, then check
// each provider name against the extension registry. This avoids a separate
// pass over all physical-network/extension combinations.
// resolveProvider() creates a transient Provider (not added to the static list)
// for extension names that are not in the built-in registry.
try {
List<PhysicalNetworkVO> physNets = _physicalNetworkDao.listAll();
if (physNets != null) {
// Use a set to avoid adding the same provider name twice (multiple phys-nets)
Set<String> addedExtProviders = new HashSet<>();
for (PhysicalNetworkVO physNet : physNets) {
List<com.cloud.network.dao.PhysicalNetworkServiceProviderVO> nsps =
_pNSPDao.listBy(physNet.getId());
if (nsps == null) continue;
for (PhysicalNetworkServiceProviderVO nsp : nsps) {
String provName = nsp.getProviderName();
if (provName == null || addedExtProviders.contains(provName)) continue;
if (!extensionHelper.isNetworkExtensionProvider(provName)) continue;
// Filter by service if requested: check the NSP's service flags
if (service != null && !isServiceProvidedByNsp(nsp, service)) continue;
// Resolve or create a transient Provider for the extension name.
Provider extProvider = resolveProvider(provName);
if (extProvider == null) continue;
supportedProviders.add(extProvider);
addedExtProviders.add(provName);
}
}
}
} catch (Exception e) {
logger.debug("Failed to include extension-backed providers in listSupportedNetworkServiceProviders: {}", e.getMessage());
}
return new ArrayList<Provider>(supportedProviders);
}
/**
* Returns {@code true} if the given {@link com.cloud.network.dao.PhysicalNetworkServiceProviderVO}
* has its service flag set for {@code service}.
*
* <p>This is used by {@link #listSupportedNetworkServiceProviders} to filter extension-backed
* providers (looked up via {@link com.cloud.network.dao.PhysicalNetworkServiceProviderDao#listBy})
* without having to query each extension's capability JSON.</p>
*/
private boolean isServiceProvidedByNsp(
PhysicalNetworkServiceProviderVO nsp, Service service) {
if (service == null) {
return true;
}
if (service == Service.Dhcp) return nsp.isDhcpServiceProvided();
if (service == Service.Dns) return nsp.isDnsServiceProvided();
if (service == Service.Firewall) return nsp.isFirewallServiceProvided();
if (service == Service.Gateway) return nsp.isGatewayServiceProvided();
if (service == Service.Lb) return nsp.isLbServiceProvided();
if (service == Service.PortForwarding) return nsp.isPortForwardingServiceProvided();
if (service == Service.SecurityGroup) return nsp.isSecuritygroupServiceProvided();
if (service == Service.SourceNat) return nsp.isSourcenatServiceProvided();
if (service == Service.StaticNat) return nsp.isStaticnatServiceProvided();
if (service == Service.UserData) return nsp.isUserdataServiceProvided();
if (service == Service.Vpn) return nsp.isVpnServiceProvided();
if (service == Service.NetworkACL) return nsp.isNetworkAclServiceProvided();
// Unknown service: fall back to true so extension-backed providers are not filtered out
return true;
}
@Override
public Provider getDefaultUniqueProviderForService(String serviceName) {
List<? extends Provider> providers = listSupportedNetworkServiceProviders(serviceName);
@ -1540,13 +1712,19 @@ public class NetworkModelImpl extends ManagerBase implements NetworkModel, Confi
throw new InvalidParameterValueException("Unable to find the Network Element implementing the Service Provider '" + provider.getName() + "'");
}
// For external network providers, get per-provider capabilities
final boolean isExternal = extensionHelper.isNetworkExtensionProvider(provider.getName());
Map<Service, Map<Capability, String>> providerCaps = isExternal
? getExternalProviderCapabilities(null, provider.getName())
: element.getCapabilities();
Set<Service> enabledServices = new HashSet<Service>();
enabledServices.addAll(providersMap.get(provider));
if (enabledServices != null && !enabledServices.isEmpty()) {
if (!element.canEnableIndividualServices()) {
if (!element.canEnableIndividualServices() && !isExternal) {
Set<Service> requiredServices = new HashSet<Service>();
requiredServices.addAll(element.getCapabilities().keySet());
requiredServices.addAll(providerCaps.keySet());
if (requiredServices.contains(Network.Service.Gateway)) {
requiredServices.remove(Network.Service.Gateway);
@ -1580,7 +1758,7 @@ public class NetworkModelImpl extends ManagerBase implements NetworkModel, Confi
List<String> serviceList = new ArrayList<String>();
for (Service service : enabledServices) {
// check if the service is provided by this Provider
if (!element.getCapabilities().containsKey(service)) {
if (!providerCaps.containsKey(service)) {
throw new UnsupportedServiceException(provider.getName() + " Provider cannot provide service " + service.getName());
}
serviceList.add(service.getName());
@ -1655,18 +1833,32 @@ public class NetworkModelImpl extends ManagerBase implements NetworkModel, Confi
return true;
}
@Override
public boolean providerSupportsCapability(Set<Provider> providers, Service service, Capability cap) {
for (Provider provider : providers) {
NetworkElement element = getElementImplementingProvider(provider.getName());
if (element != null) {
Map<Service, Map<Capability, String>> elementCapabilities = element.getCapabilities();
boolean isExtProvider = extensionHelper.isNetworkExtensionProvider(provider.getName());
Map<Service, Map<Capability, String>> elementCapabilities = isExtProvider
? getExternalProviderCapabilities(null, provider.getName())
: element.getCapabilities();
if (elementCapabilities == null || !elementCapabilities.containsKey(service)) {
if (isExtProvider) {
// Extension provider with no declared capabilities for this service
// treat as "service supported but capability not constrained"
return false;
}
throw new UnsupportedServiceException("Service " + service.getName() + " is not supported by the element=" + element.getName() +
" implementing Provider=" + provider.getName());
}
Map<Capability, String> serviceCapabilities = elementCapabilities.get(service);
if (serviceCapabilities == null || serviceCapabilities.isEmpty()) {
if (isExtProvider) {
// Extension provider declared the service but without specific capability
// constraints treat as "capability not constrained, not explicitly supported"
return false;
}
throw new UnsupportedServiceException("Service " + service.getName() + " doesn't have capabilites for element=" + element.getName() +
" implementing Provider=" + provider.getName());
}
@ -1686,19 +1878,36 @@ public class NetworkModelImpl extends ManagerBase implements NetworkModel, Confi
for (Provider provider : providers) {
NetworkElement element = getElementImplementingProvider(provider.getName());
if (element != null) {
Map<Service, Map<Capability, String>> elementCapabilities = element.getCapabilities();
boolean isExtProvider = extensionHelper.isNetworkExtensionProvider(provider.getName());
Map<Service, Map<Capability, String>> elementCapabilities = isExtProvider
? getExternalProviderCapabilities(null, provider.getName())
: element.getCapabilities();
if (elementCapabilities == null || !elementCapabilities.containsKey(service)) {
if (isExtProvider) {
// Extension provider with no declared capabilities for this service
// treat as supported without constraints; skip the capability check.
continue;
}
throw new UnsupportedServiceException("Service " + service.getName() + " is not supported by the element=" + element.getName() +
" implementing Provider=" + provider.getName());
}
Map<Capability, String> serviceCapabilities = elementCapabilities.get(service);
if (serviceCapabilities == null || serviceCapabilities.isEmpty()) {
if (isExtProvider) {
// Extension provider declared the service without capability constraints
// accept any capability value (the extension handles it at runtime).
continue;
}
throw new UnsupportedServiceException("Service " + service.getName() + " doesn't have capabilities for element=" + element.getName() +
" implementing Provider=" + provider.getName());
}
String value = serviceCapabilities.get(cap);
if (value == null || value.isEmpty()) {
if (isExtProvider) {
// Capability not explicitly declared for this extension accept it.
continue;
}
throw new UnsupportedServiceException("Service " + service.getName() + " doesn't have capability " + cap.getName() + " for element=" +
element.getName() + " implementing Provider=" + provider.getName());
}
@ -1953,7 +2162,7 @@ public class NetworkModelImpl extends ManagerBase implements NetworkModel, Confi
List<String> providerNames = _ntwkOfferingSrvcDao.getDistinctProviders(ntkwOffId);
List<Provider> providers = new ArrayList<Provider>();
for (String providerName : providerNames) {
providers.add(Network.Provider.getProvider(providerName));
providers.add(resolveProvider(providerName));
}
return providers;
@ -2255,7 +2464,7 @@ public class NetworkModelImpl extends ManagerBase implements NetworkModel, Confi
Map<String, Provider> providers = new HashMap<String, Provider>();
for (String providerName : providerNames) {
if (!providers.containsKey(providerName)) {
providers.put(providerName, Network.Provider.getProvider(providerName));
providers.put(providerName, resolveProvider(providerName));
}
}

View File

@ -68,6 +68,8 @@ import org.apache.cloudstack.api.command.user.vm.ListNicsCmd;
import org.apache.cloudstack.api.response.AcquirePodIpCmdResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService;
import org.apache.cloudstack.extension.ExtensionResourceMap;
import org.apache.cloudstack.framework.extensions.manager.ExtensionsManager;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.framework.config.Configurable;
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
@ -400,6 +402,8 @@ public class NetworkServiceImpl extends ManagerBase implements NetworkService, C
@Inject
NetworkDetailsDao _networkDetailsDao;
@Inject
ExtensionsManager extensionsManager;
@Inject
LoadBalancerDao _loadBalancerDao;
@Inject
NetworkMigrationManager _networkMigrationManager;
@ -3087,8 +3091,8 @@ public class NetworkServiceImpl extends ManagerBase implements NetworkService, C
protected boolean providersConfiguredForExternalNetworking(Collection<String> providers) {
for (String providerStr : providers) {
Provider provider = Network.Provider.getProvider(providerStr);
if (provider.isExternal()) {
Provider provider = _networkModel.resolveProvider(providerStr);
if (provider != null && provider.isExternal()) {
return true;
}
}
@ -4395,7 +4399,7 @@ public class NetworkServiceImpl extends ManagerBase implements NetworkService, C
@Override
@DB
@ActionEvent(eventType = EventTypes.EVENT_PHYSICAL_NETWORK_UPDATE, eventDescription = "updating physical network", async = true)
public PhysicalNetwork updatePhysicalNetwork(Long id, String networkSpeed, List<String> tags, String newVnetRange, String state) {
public PhysicalNetwork updatePhysicalNetwork(Long id, String networkSpeed, List<String> tags, String newVnetRange, String state, Map<String, String> externalDetails) {
// verify input parameters
PhysicalNetworkVO network = _physicalNetworkDao.findById(id);
@ -4449,6 +4453,29 @@ public class NetworkServiceImpl extends ManagerBase implements NetworkService, C
addOrRemoveVnets(listOfRanges, network);
}
_physicalNetworkDao.update(id, network);
// If external details provided, and an extension is registered on this physical network,
// update the extension_resource_map_details accordingly.
try {
if (externalDetails != null && !externalDetails.isEmpty()) {
Pair<Boolean, ExtensionResourceMap> needDetailsUpdateMapPair =
extensionsManager.extensionResourceMapDetailsNeedUpdate(id,
ExtensionResourceMap.ResourceType.PhysicalNetwork, externalDetails);
if (Boolean.TRUE.equals(needDetailsUpdateMapPair.first())) {
ExtensionResourceMap extensionResourceMap = needDetailsUpdateMapPair.second();
if (extensionResourceMap == null) {
throw new InvalidParameterValueException(
String.format("Physical network: %s is not registered with any extension, details cannot be updated",
network.getId()));
}
extensionsManager.updateExtensionResourceMapDetails(extensionResourceMap.getId(), externalDetails);
}
}
} catch (Exception e) {
// Log warning but don't fail the update
logger.warn("Failed to update external details for physical network {}: {}", id, e.getMessage());
}
return network;
}
@ -5121,7 +5148,7 @@ public class NetworkServiceImpl extends ManagerBase implements NetworkService, C
Provider provider = null;
if (providerName != null) {
provider = Network.Provider.getProvider(providerName);
provider = _networkModel.resolveProvider(providerName);
if (provider == null) {
throw new InvalidParameterValueException("Invalid Network Service Provider=" + providerName);
}
@ -5158,7 +5185,7 @@ public class NetworkServiceImpl extends ManagerBase implements NetworkService, C
}
if (providerName != null) {
Provider provider = Network.Provider.getProvider(providerName);
Provider provider = _networkModel.resolveProvider(providerName);
if (provider == null) {
throw new InvalidParameterValueException("Invalid Network Service Provider=" + providerName);
}

View File

@ -1468,7 +1468,7 @@ public class AutoScaleManagerImpl extends ManagerBase implements AutoScaleManage
}
// Validate Provider
Network.Provider provider = Network.Provider.getProvider(cmd.getProvider());
Network.Provider provider = networkModel.resolveProvider(cmd.getProvider());
if (provider == null) {
throw new InvalidParameterValueException("The Provider " + cmd.getProvider() + " does not exist; Unable to create Counter");
}
@ -1537,7 +1537,7 @@ public class AutoScaleManagerImpl extends ManagerBase implements AutoScaleManage
}
String providerStr = cmd.getProvider();
if (providerStr != null) {
Network.Provider provider = Network.Provider.getProvider(providerStr);
Network.Provider provider = networkModel.resolveProvider(providerStr);
if (provider == null) {
throw new InvalidParameterValueException("The Provider " + providerStr + " does not exist; Unable to list Counter");
}

View File

@ -69,9 +69,11 @@ import com.cloud.network.dao.FirewallRulesDcidrsDao;
import com.cloud.network.dao.IPAddressDao;
import com.cloud.network.dao.IPAddressVO;
import com.cloud.network.dao.NetworkDao;
import com.cloud.network.dao.NetworkServiceMapDao;
import com.cloud.network.dao.NetworkVO;
import com.cloud.network.element.FirewallServiceProvider;
import com.cloud.network.element.NetworkACLServiceProvider;
import com.cloud.network.element.NetworkElement;
import com.cloud.network.element.PortForwardingServiceProvider;
import com.cloud.network.element.StaticNatServiceProvider;
import com.cloud.network.rules.FirewallManager;
@ -150,6 +152,9 @@ public class FirewallManagerImpl extends ManagerBase implements FirewallService,
EntityManager entityManager;
@Inject
NsxProviderDao nsxProviderDao;
@Inject
NetworkServiceMapDao networkServiceMapDao;
List<FirewallServiceProvider> _firewallElements;
List<PortForwardingServiceProvider> _pfElements;
@ -617,12 +622,25 @@ public class FirewallManagerImpl extends ManagerBase implements FirewallService,
String supportedProtocols;
String supportedTrafficTypes = null;
if (purpose == FirewallRule.Purpose.Firewall) {
supportedTrafficTypes = caps.get(Capability.SupportedTrafficDirection).toLowerCase();
String supportedTrafficTypesStr = caps.get(Capability.SupportedTrafficDirection);
if (supportedTrafficTypesStr == null) {
throw new CloudRuntimeException("Supported traffic direction capability is not defined for Firewall service");
}
supportedTrafficTypes = supportedTrafficTypesStr.toLowerCase();
}
if (purpose == FirewallRule.Purpose.Firewall && trafficType == FirewallRule.TrafficType.Egress) {
// throw an exception if cap is not found
String supportedProtocolsStr = caps.get(Capability.SupportedEgressProtocols);
if (supportedProtocolsStr == null) {
throw new CloudRuntimeException("Supported egress protocols capability is not defined for Firewall service");
}
supportedProtocols = caps.get(Capability.SupportedEgressProtocols).toLowerCase();
} else {
String supportedProtocolsStr = caps.get(Capability.SupportedProtocols);
if (supportedProtocolsStr == null) {
throw new CloudRuntimeException("Supported protocols capability is not defined for " + purpose + " service");
}
supportedProtocols = caps.get(Capability.SupportedProtocols).toLowerCase();
}
@ -700,18 +718,34 @@ public class FirewallManagerImpl extends ManagerBase implements FirewallService,
if (handled)
break;
}
if (!handled) {
// Get provider name and get the element by provider name (it could be an external provider)
String fwProviderName = networkServiceMapDao.getProviderForServiceInNetwork(network.getId(), Service.Firewall);
if (fwProviderName != null) {
NetworkElement element = _networkModel.getElementImplementingProvider(fwProviderName);
handled = ((FirewallServiceProvider) element).applyFWRules(network, rules);
}
}
break;
case PortForwarding:
for (PortForwardingServiceProvider element : _pfElements) {
for (PortForwardingServiceProvider element : _pfElements) {
Network.Provider provider = element.getProvider();
boolean isPfProvider = _networkModel.isProviderSupportServiceInNetwork(network.getId(), Service.PortForwarding, provider);
if (!isPfProvider) {
continue;
}
handled = element.applyPFRules(network, (List<PortForwardingRule>)rules);
handled = element.applyPFRules(network, (List<PortForwardingRule>)rules);
if (handled)
break;
}
if (!handled) {
// Get provider name and get the element by provider name (it could be an external provider)
String pfProviderName = networkServiceMapDao.getProviderForServiceInNetwork(network.getId(), Service.PortForwarding);
if (pfProviderName != null) {
NetworkElement element = _networkModel.getElementImplementingProvider(pfProviderName);
handled = ((PortForwardingServiceProvider) element).applyPFRules(network, (List<PortForwardingRule>) rules);
}
}
break;
/* case NetworkACL:
for (NetworkACLServiceProvider element: _networkAclElements) {
@ -726,7 +760,7 @@ public class FirewallManagerImpl extends ManagerBase implements FirewallService,
}
break;*/
default:
assert (false) : "Unexpected fall through in applying rules to the network elements";
assert (false) : "Unexpected fall through in applying rules to the network elements";
logger.error("FirewallManager cannot process rules of type " + purpose);
throw new CloudRuntimeException("FirewallManager cannot process rules of type " + purpose);
}

View File

@ -119,6 +119,7 @@ import com.cloud.network.dao.NetworkServiceMapDao;
import com.cloud.network.dao.NetworkVO;
import com.cloud.network.dao.SslCertVO;
import com.cloud.network.element.LoadBalancingServiceProvider;
import com.cloud.network.element.NetworkElement;
import com.cloud.network.lb.LoadBalancingRule.LbAutoScalePolicy;
import com.cloud.network.lb.LoadBalancingRule.LbAutoScaleVmGroup;
import com.cloud.network.lb.LoadBalancingRule.LbAutoScaleVmProfile;
@ -2049,6 +2050,14 @@ public class LoadBalancingRulesManagerImpl<Type> extends ManagerBase implements
if (handled)
break;
}
if (!handled) {
// Get provider name and get the element by provider name (it could be an external provider)
String lbProviderName = _ntwkSrvcDao.getProviderForServiceInNetwork(network.getId(), Service.Lb);
if (lbProviderName != null) {
NetworkElement element = _networkModel.getElementImplementingProvider(lbProviderName);
handled = ((LoadBalancingServiceProvider) element).applyLBRules(network, rules);
}
}
return handled;
}

View File

@ -33,8 +33,10 @@ import com.cloud.network.Network;
import com.cloud.network.Network.Service;
import com.cloud.network.NetworkModel;
import com.cloud.network.dao.NetworkDao;
import com.cloud.network.dao.NetworkServiceMapDao;
import com.cloud.network.dao.NetworkVO;
import com.cloud.network.element.NetworkACLServiceProvider;
import com.cloud.network.element.NetworkElement;
import com.cloud.network.element.VpcProvider;
import com.cloud.network.vpc.NetworkACLItem.State;
import com.cloud.network.vpc.dao.NetworkACLDao;
@ -75,6 +77,8 @@ public class NetworkACLManagerImpl extends ManagerBase implements NetworkACLMana
private MessageBus _messageBus;
@Inject
private ResourceTagDao resourceTagDao;
@Inject
NetworkServiceMapDao networkServiceMapDao;
private List<NetworkACLServiceProvider> _networkAclElements;
@ -442,12 +446,23 @@ public class NetworkACLManagerImpl extends ManagerBase implements NetworkACLMana
logger.debug("Applying NetworkACL for network: {} with Network ACL service provider", network);
handled = element.applyNetworkACLs(network, rules);
if (handled) {
// publish message on message bus, so that network elements implementing distributed routing
// capability can act on the event
_messageBus.publish(_name, "Network_ACL_Replaced", PublishScope.LOCAL, network);
break;
}
}
if (!foundProvider) {
// Get provider name and get the element by provider name (it could be an external provider)
String aclProviderName = networkServiceMapDao.getProviderForServiceInNetwork(network.getId(), Service.NetworkACL);
if (aclProviderName != null) {
foundProvider = true;
NetworkElement element = _networkModel.getElementImplementingProvider(aclProviderName);
handled = ((NetworkACLServiceProvider) element).applyNetworkACLs(network, rules);
}
}
if (handled) {
// publish message on message bus, so that network elements implementing distributed routing
// capability can act on the event
_messageBus.publish(_name, "Network_ACL_Replaced", PublishScope.LOCAL, network);
}
if (!foundProvider) {
logger.debug("Unable to find NetworkACL service provider for network: {}", network);
}

View File

@ -64,6 +64,8 @@ import org.apache.cloudstack.api.command.user.vpc.RestartVPCCmd;
import org.apache.cloudstack.api.command.user.vpc.UpdateVPCCmd;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService;
import org.apache.cloudstack.extension.Extension;
import org.apache.cloudstack.extension.ExtensionHelper;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.framework.config.Configurable;
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
@ -329,6 +331,8 @@ public class VpcManagerImpl extends ManagerBase implements VpcManager, VpcProvis
Site2SiteVpnConnectionDao site2SiteVpnConnectionDao;
@Inject
Site2SiteCustomerGatewayDao site2SiteCustomerGatewayDao;
@Inject
ExtensionHelper extensionHelper;
private final ScheduledExecutorService _executor = Executors.newScheduledThreadPool(1, new NamedThreadFactory("VpcChecker"));
private List<VpcProvider> vpcElements = null;
@ -701,7 +705,7 @@ public class VpcManagerImpl extends ManagerBase implements VpcManager, VpcProvis
final Set<Provider> providers = new HashSet<Provider>();
for (final String prvNameStr : serviceEntry.getValue()) {
// check if provider is supported
final Network.Provider provider = Network.Provider.getProvider(prvNameStr);
final Network.Provider provider = _ntwkModel.resolveProvider(prvNameStr);
if (provider == null) {
throw new InvalidParameterValueException("Invalid service provider: " + prvNameStr);
}
@ -1248,12 +1252,18 @@ public class VpcManagerImpl extends ManagerBase implements VpcManager, VpcProvis
for (final VpcOfferingServiceMapVO instance : map) {
final Service service = Service.getService(instance.getService());
if (service == null) {
continue;
}
Set<Provider> providers;
providers = serviceProviderMap.get(service);
if (providers == null) {
providers = new HashSet<Provider>();
}
providers.add(Provider.getProvider(instance.getProvider()));
final Provider provider = _ntwkModel.resolveProvider(instance.getProvider());
if (provider != null) {
providers.add(provider);
}
serviceProviderMap.put(service, providers);
}
@ -1846,6 +1856,8 @@ public class VpcManagerImpl extends ManagerBase implements VpcManager, VpcProvis
if (provider == null) {
// Default to VPCVirtualRouter
provider = Provider.VPCVirtualRouter.getName();
} else {
provider = _ntwkModel.resolveProvider(provider).getName();
}
if (!_ntwkModel.isProviderEnabledInZone(zoneId, provider)) {
@ -2035,9 +2047,12 @@ public class VpcManagerImpl extends ManagerBase implements VpcManager, VpcProvis
if (! userIps.isEmpty()) {
try {
_ipAddrMgr.updateSourceNatIpAddress(requestedIp, userIps);
if (isVpcForProvider(Provider.Nsx, vpc) || isVpcForProvider(Provider.Netris, vpc)) {
if (isVpcForProvider(Provider.Nsx, vpc) || isVpcForProvider(Provider.Netris, vpc)
|| isVpcForProvider(Provider.NetworkExtension, vpc)) {
boolean isForNsx = _vpcOffSvcMapDao.isProviderForVpcOffering(Provider.Nsx, vpc.getVpcOfferingId());
String providerName = isForNsx ? Provider.Nsx.getName() : Provider.Netris.getName();
boolean isForNetris = _vpcOffSvcMapDao.isProviderForVpcOffering(Provider.Netris, vpc.getVpcOfferingId());
String providerName = isForNsx ? Provider.Nsx.getName()
: (isForNetris ? Provider.Netris.getName() : Provider.NetworkExtension.getName());
VpcProvider providerElement = (VpcProvider) _ntwkModel.getElementImplementingProvider(providerName);
if (Objects.nonNull(providerElement)) {
providerElement.updateVpcSourceNatIp(vpc, requestedIp);
@ -2511,7 +2526,8 @@ public class VpcManagerImpl extends ManagerBase implements VpcManager, VpcProvis
// 1) in current release, only vpc provider is supported by Vpc offering
final List<Provider> providers = _ntwkModel.getNtwkOffDistinctProviders(guestNtwkOff.getId());
for (final Provider provider : providers) {
if (!supportedProviders.contains(provider)) {
if (!supportedProviders.contains(provider)
&& !extensionHelper.isNetworkExtensionProvider(provider.getName())) {
throw new InvalidParameterValueException("Provider of type " + provider.getName() + " is not supported for network offerings that can be used in VPC");
}
}
@ -2618,17 +2634,32 @@ public class VpcManagerImpl extends ManagerBase implements VpcManager, VpcProvis
}
public List<VpcProvider> getVpcElements() {
// Static providers (VPCVirtualRouter, JuniperContrailVpcRouter) are initialized once.
if (vpcElements == null) {
vpcElements = new ArrayList<VpcProvider>();
vpcElements.add((VpcProvider) _ntwkModel.getElementImplementingProvider(Provider.VPCVirtualRouter.getName()));
vpcElements.add((VpcProvider) _ntwkModel.getElementImplementingProvider(Provider.JuniperContrailVpcRouter.getName()));
final NetworkElement vpcVirtualRouter = _ntwkModel.getElementImplementingProvider(Provider.VPCVirtualRouter.getName());
if (vpcVirtualRouter instanceof VpcProvider) {
vpcElements.add((VpcProvider) vpcVirtualRouter);
}
final NetworkElement contrailVpcRouter = _ntwkModel.getElementImplementingProvider(Provider.JuniperContrailVpcRouter.getName());
if (contrailVpcRouter instanceof VpcProvider) {
vpcElements.add((VpcProvider) contrailVpcRouter);
}
}
if (vpcElements == null) {
throw new CloudRuntimeException("Failed to initialize vpc elements");
// Extension-backed providers are re-fetched every call so that dynamically
// registered extensions are picked up without requiring a server restart.
final List<VpcProvider> result = new ArrayList<>(vpcElements);
for (final Extension extension : extensionHelper.listExtensionsByType(Extension.Type.NetworkOrchestrator)) {
final String providerName = extension.getName();
final NetworkElement element = _ntwkModel.getElementImplementingProvider(providerName);
if (element instanceof VpcProvider) {
result.add((VpcProvider) element);
}
}
return vpcElements;
return result;
}
@Override
@ -3937,7 +3968,7 @@ public class VpcManagerImpl extends ManagerBase implements VpcManager, VpcProvis
final Map<String, Provider> providers = new HashMap<String, Provider>();
for (final String providerName : providerNames) {
if (!providers.containsKey(providerName)) {
providers.put(providerName, Network.Provider.getProvider(providerName));
providers.put(providerName, _ntwkModel.resolveProvider(providerName));
}
}

View File

@ -400,6 +400,16 @@ public class MockNetworkModelImpl extends ManagerBase implements NetworkModel {
return false;
}
@Override
public boolean canElementEnableIndividualServicesByName(String providerName) {
return false;
}
@Override
public Provider resolveProvider(String providerName) {
return Provider.getProvider(providerName);
}
/* (non-Javadoc)
* @see com.cloud.network.NetworkModel#areServicesSupportedInNetwork(long, com.cloud.network.Network.Service[])
*/

View File

@ -66,6 +66,7 @@ import com.cloud.utils.net.Ip;
import com.cloud.vm.Nic;
import com.cloud.vm.NicProfile;
import com.cloud.vm.VirtualMachine;
import org.apache.cloudstack.extension.ExtensionHelper;
@RunWith(MockitoJUnitRunner.class)
public class NetworkModelImplTest {
@ -80,6 +81,8 @@ public class NetworkModelImplTest {
private NetworkDao _networksDao;
@Inject
private NetworkOfferingServiceMapDao networkOfferingServiceMapDao;
@Mock
private ExtensionHelper extensionHelper;
@Spy
@InjectMocks
@ -96,6 +99,8 @@ public class NetworkModelImplTest {
networkModel._networkOfferingDao = networkOfferingDao;
networkModel._ntwkSrvcDao = networkServiceMapDao;
networkModel._ntwkOfferingSrvcDao = networkOfferingServiceMapDao;
ReflectionTestUtils.setField(networkModel, "extensionHelper", extensionHelper);
Mockito.lenient().when(extensionHelper.isNetworkExtensionProvider(Mockito.anyString())).thenReturn(false);
}
private void prepareMocks(boolean isIp6, Network network, DataCenter zone, VpcVO vpc,
@ -242,8 +247,8 @@ public class NetworkModelImplTest {
networkOfferingVO.setForVpc(true);
Network network = new NetworkVO();
List<NetworkServiceMapVO> networkServiceMapVOs = new ArrayList<>();
networkServiceMapVOs.add(new NetworkServiceMapVO(15L, Network.Service.Firewall, Network.Provider.VPCVirtualRouter));
networkServiceMapVOs.add(new NetworkServiceMapVO(15L, Network.Service.SourceNat, Network.Provider.VPCVirtualRouter));
networkServiceMapVOs.add(new NetworkServiceMapVO(15L, Network.Service.Firewall.getName(), Network.Provider.VPCVirtualRouter.getName()));
networkServiceMapVOs.add(new NetworkServiceMapVO(15L, Network.Service.SourceNat.getName(), Network.Provider.VPCVirtualRouter.getName()));
NetworkElement element = new VpcVirtualRouterElement();
ReflectionTestUtils.setField(networkModel, "networkElements", List.of(element));

View File

@ -423,6 +423,8 @@ public class AutoScaleManagerImplTest {
when(conditionDao.findById(any())).thenReturn(conditionMock);
when(conditionDao.persist(any(ConditionVO.class))).thenReturn(conditionMock);
when(networkModel.resolveProvider(counterProvider)).thenReturn(Network.Provider.VirtualRouter);
when(accountManager.finalizeOwner(nullable(Account.class), nullable(String.class), nullable(Long.class), nullable(Long.class))).thenReturn(account);
Mockito.doNothing().when(accountManager).checkAccess(Mockito.any(Account.class), Mockito.isNull(), Mockito.anyBoolean(), Mockito.any());

View File

@ -62,6 +62,8 @@ import com.cloud.vm.dao.VMInstanceDao;
import com.google.common.collect.Maps;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService;
import org.apache.cloudstack.extension.ExtensionHelper;
import org.springframework.test.util.ReflectionTestUtils;
import org.apache.cloudstack.engine.subsystem.api.storage.DataStore;
import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager;
import org.apache.cloudstack.engine.subsystem.api.storage.EndPoint;
@ -151,6 +153,7 @@ public class ConfigDriveNetworkElementTest {
@Mock private CallContext callContextMock;
@Mock private DomainVO domainVO;
@Mock private NetworkOrchestrationService _networkOrchestrationService;
@Mock private ExtensionHelper extensionHelper;
@Spy @InjectMocks
private ConfigDriveNetworkElement _configDrivesNetworkElement = new ConfigDriveNetworkElement();
@ -202,6 +205,8 @@ public class ConfigDriveNetworkElementTest {
doReturn(_configDrivesNetworkElement.getProvider().getName()).when(_ntwkSrvcDao).getProviderForServiceInNetwork(NETWORK_ID, Network.Service.UserData);
_networkModel.setNetworkElements(Arrays.asList(_configDrivesNetworkElement));
ReflectionTestUtils.setField(_networkModel, "extensionHelper", extensionHelper);
Mockito.lenient().when(extensionHelper.isNetworkExtensionProvider(Mockito.anyString())).thenReturn(false);
_networkModel.start();
}

View File

@ -68,7 +68,6 @@ import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@RunWith(SpringJUnit4ClassRunner.class)
@ -92,6 +91,8 @@ public class NetworkACLManagerTest extends TestCase {
@Inject
NetworkModel _networkModel;
@Inject
NetworkServiceMapDao networkServiceMapDao;
@Inject
List<NetworkACLServiceProvider> _networkAclElements;
@Inject
VpcService _vpcSvc;
@ -169,8 +170,7 @@ public class NetworkACLManagerTest extends TestCase {
final List<NetworkVO> networks = new ArrayList<>();
networks.add(network);
NetworkServiceMapDao ntwkSrvcDao = mock(NetworkServiceMapDao.class);
when(ntwkSrvcDao.canProviderSupportServiceInNetwork(anyLong(), eq(Network.Service.NetworkACL), nullable(Network.Provider.class))).thenReturn(true);
when(networkServiceMapDao.canProviderSupportServiceInNetwork(anyLong(), eq(Network.Service.NetworkACL), nullable(Network.Provider.class))).thenReturn(true);
Mockito.when(_networkDao.listByAclId(anyLong())).thenReturn(networks);
Mockito.when(_networkDao.findById(anyLong())).thenReturn(network);
Mockito.when(networkOfferingDao.isIpv6Supported(anyLong())).thenReturn(false);
@ -277,6 +277,11 @@ public class NetworkACLManagerTest extends TestCase {
return Mockito.mock(NetworkModel.class);
}
@Bean
public NetworkServiceMapDao networkServiceMapDao() {
return Mockito.mock(NetworkServiceMapDao.class);
}
@Bean
public VpcManager vpcManager() {
return Mockito.mock(VpcManager.class);

View File

@ -99,6 +99,7 @@ import java.util.UUID;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
@ -247,14 +248,21 @@ public class VpcManagerImplTest {
@Test
public void getVpcOffSvcProvidersMapForEmptyServiceTest() {
long vpcOffId = 1L;
VpcOfferingServiceMapVO svcMap = mock(VpcOfferingServiceMapVO.class);
Mockito.when(svcMap.getService()).thenReturn(Service.SourceNat.getName());
Mockito.when(svcMap.getProvider()).thenReturn(Provider.VPCVirtualRouter.getName());
Mockito.when(networkModel.resolveProvider(Provider.VPCVirtualRouter.getName()))
.thenReturn(Provider.VPCVirtualRouter);
List<VpcOfferingServiceMapVO> list = new ArrayList<VpcOfferingServiceMapVO>();
list.add(mock(VpcOfferingServiceMapVO.class));
list.add(svcMap);
Mockito.when(manager._vpcOffSvcMapDao.listByVpcOffId(vpcOffId)).thenReturn(list);
Map<Service, Set<Provider>> map = manager.getVpcOffSvcProvidersMap(vpcOffId);
assertNotNull(map);
assertEquals(map.size(), 1);
assertTrue(map.containsKey(Service.SourceNat));
assertTrue(map.get(Service.SourceNat).contains(Provider.VPCVirtualRouter));
}
protected Map<String, String> createFakeCapabilityInputMap() {

View File

@ -345,7 +345,7 @@ public class MockNetworkManagerImpl extends ManagerBase implements NetworkOrches
* @see com.cloud.network.NetworkService#updatePhysicalNetwork(java.lang.Long, java.lang.String, java.util.List, java.lang.String, java.lang.String)
*/
@Override
public PhysicalNetwork updatePhysicalNetwork(Long id, String networkSpeed, List<String> tags, String newVnetRangeString, String state) {
public PhysicalNetwork updatePhysicalNetwork(Long id, String networkSpeed, List<String> tags, String newVnetRangeString, String state, Map<String, String> externalDetails) {
// TODO Auto-generated method stub
return null;
}

View File

@ -411,6 +411,15 @@ public class MockNetworkModelImpl extends ManagerBase implements NetworkModel {
return false;
}
@Override
public Provider resolveProvider(String providerName) {
return Provider.getProvider(providerName);
}
@Override
public boolean canElementEnableIndividualServicesByName(String providerName) {
return false;
}
/* (non-Javadoc)
* @see com.cloud.network.NetworkModel#areServicesSupportedInNetwork(long, com.cloud.network.Network.Service[])
*/

File diff suppressed because it is too large Load Diff

View File

@ -241,6 +241,7 @@
"label.action.unmanage.volume": "Unmanage Volume",
"label.action.unmanage.volumes": "Unmanage Volumes",
"label.action.unregister.extension.resource": "Unregister extension resource",
"label.action.update.extension.resource": "Update extension resource details",
"label.action.update.host": "Update Host",
"label.action.update.security.groups": "Update security groups",
"label.action.update.offering.access": "Update offering access",
@ -4176,6 +4177,8 @@
"message.validate.min": "Please enter a value greater than or equal to {0}.",
"message.action.delete.object.storage": "Please confirm that you want to delete this Object Store",
"message.action.unregister.extension.resource": "Please confirm that you want to unregister extension with this resource",
"message.action.update.extension.resource": "Update the extension resource registration details",
"message.success.update.extension.resource": "Successfully updated extension resource registration",
"message.bgp.peers.null": "Please note, if no BGP peers are selected, the VR will connect to <br> (1) dedicated BGP peers the owner can access, if the owner has dedicated BGP peers and account setting use.system.bgp.peers is set to false; <br> (2) all BGP peers the owner can access, otherwise.<br>",
"message.bucket.delete": "Please confirm that you want to delete this Bucket",
"migrate.from": "Migrate from",
@ -4238,5 +4241,21 @@
"Compute*Month": "Compute * Month",
"GB*Month": "GB * Month",
"IP*Month": "IP * Month",
"Policy*Month": "Policy * Month"
"Policy*Month": "Policy * Month",
"ExternalNetwork": "External Network",
"label.external.network": "External Network",
"label.external.network.provider": "External Network Provider",
"label.extension": "Extension",
"label.services": "Services",
"label.add.external.network.provider": "Add External Network Provider",
"label.not.added": "Not Added",
"label.refresh": "Refresh",
"label.run.action": "Run Action",
"label.enable.provider": "Enable Provider",
"label.disable.provider": "Disable Provider",
"label.external.network.service": "External Network Service",
"message.confirm.disable.external.network.provider": "Are you sure you want to disable the External Network provider?",
"message.no.network.orchestrator.extensions": "No NetworkOrchestrator extensions found. Please create one first via createExtension API.",
"message.extension.services.from.capabilities": "Services are derived automatically from the extension's network.capabilities detail.",
"message.select.extension": "Please select an extension."
}

View File

@ -45,7 +45,7 @@ export default {
return fields
},
details: ['name', 'description', 'id', 'type', 'details', 'path', 'pathready', 'isuserdefined', 'orchestratorrequirespreparevm', 'reservedresourcedetails', 'created'],
filters: ['orchestrator'],
filters: ['orchestrator', 'networkorchestrator'],
tabs: [{
name: 'details',
component: shallowRef(defineAsyncComponent(() => import('@/components/view/DetailsTab.vue')))

View File

@ -94,7 +94,7 @@ export default {
icon: 'edit-outlined',
label: 'label.update.physical.network',
dataView: true,
args: ['vlan', 'tags']
args: ['vlan', 'tags', 'externaldetails']
},
{
api: 'addTrafficType',

View File

@ -207,6 +207,20 @@ export default {
}
}
},
{
api: 'runCustomAction',
icon: 'thunderbolt-outlined',
label: 'label.run.action',
dataView: true,
show: (record) => {
return 'runCustomAction' in store.getters.apis &&
'listCustomActions' in store.getters.apis &&
record.service && record.service.some(s =>
s.provider && s.provider.some(p => p.name === 'ExternalNetwork'))
},
popup: true,
component: shallowRef(defineAsyncComponent(() => import('@/views/extension/RunCustomAction.vue')))
},
{
api: 'deleteNetwork',
icon: 'delete-outlined',

View File

@ -34,8 +34,25 @@
api="listExtensions"
:apiParams="extensionsApiParams"
resourceType="extension"
@change-option="updateResourceTypeByExtension"
defaultIcon="appstore-add-outlined" />
</a-form-item>
<a-form-item name="resourcetype" ref="resourcetype">
<template #label>
<tooltip-label :title="$t('label.resourcetype')" :tooltip="apiParams.resourcetype.description"/>
</template>
<a-select
v-model:value="form.resourcetype"
:placeholder="apiParams.resourcetype.description"
optionFilterProp="label"
:filterOption="(input, option) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}" >
<a-select-option v-for="opt in resourceTypeOptions" :key="opt" :label="opt">
{{ opt }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item name="name" ref="name">
<template #label>
<tooltip-label :title="$t('label.name')" :tooltip="apiParams.name.description"/>
@ -152,6 +169,7 @@ export default {
data () {
return {
roleTypes: [],
resourceTypeOptions: ['VirtualMachine', 'Network'],
loading: false
}
},
@ -165,6 +183,7 @@ export default {
this.initForm()
if (this.extension) {
this.form.extensionid = this.extension.id
this.updateResourceTypeByExtension(this.extension)
}
this.roleTypes = this.$fetchCustomActionRoleTypes()
},
@ -173,13 +192,30 @@ export default {
this.formRef = ref()
this.form = reactive({
enabled: true,
timeout: 5
timeout: 5,
resourcetype: 'VirtualMachine'
})
this.rules = reactive({
extensionid: [{ required: true, message: `${this.$t('message.error.select')}` }],
name: [{ required: true, message: `${this.$t('message.error.name')}` }]
name: [{ required: true, message: `${this.$t('message.error.name')}` }],
resourcetype: [{ required: true, message: `${this.$t('message.error.select')}` }]
})
},
updateResourceTypeByExtension (selectedExtension) {
const type = selectedExtension?.type
if (type === 'NetworkOrchestrator') {
this.resourceTypeOptions = ['Network']
this.form.resourcetype = 'Network'
} else if (type === 'Orchestrator') {
this.resourceTypeOptions = ['VirtualMachine']
this.form.resourcetype = 'VirtualMachine'
} else {
this.resourceTypeOptions = ['VirtualMachine', 'Network']
if (!this.form.resourcetype) {
this.form.resourcetype = 'VirtualMachine'
}
}
},
handleSubmit (e) {
e.preventDefault()
if (this.loading) return
@ -189,6 +225,7 @@ export default {
const params = {
extensionid: values.extensionid || this.extension.id,
name: values.name,
resourcetype: values.resourcetype,
enabled: values.enabled
}
const keys = ['description', 'allowedroletypes', 'successmessage', 'errormessage', 'timeout']

View File

@ -162,7 +162,7 @@ export default {
},
fetchExtensionTypes () {
this.extensionTypes = []
const extensionTypesList = ['Orchestrator']
const extensionTypesList = ['Orchestrator', 'NetworkOrchestrator']
extensionTypesList.forEach((item) => {
this.extensionTypes.push({
id: item,

View File

@ -34,6 +34,13 @@
{{ text && $toLocaleDate(text) }}
</template>
<template v-if="column.key === 'actions'">
<span style="margin-right: 5px">
<tooltip-button
:tooltip="$t('label.action.update.extension.resource')"
type="default"
icon="edit-outlined"
@onClick="openUpdateModal(record)" />
</span>
<span style="margin-right: 5px">
<a-popconfirm
v-if="'unregisterExtension' in $store.getters.apis"
@ -59,6 +66,20 @@
:data-map="record.details" />
</template>
</a-table>
<a-modal
v-if="updateModalVisible"
:visible="updateModalVisible"
:width="600"
:title="$t('label.action.update.extension.resource')"
:closable="true"
:footer="null"
@cancel="closeUpdateModal">
<update-registered-extension
:resource="resource"
:extension-resource="selectedResource"
@refresh-data="handleRefreshData"
@close-action="closeUpdateModal" />
</a-modal>
</div>
</template>
@ -67,13 +88,16 @@ import { postAPI } from '@/api'
import eventBus from '@/config/eventBus'
import ObjectListTable from '@/components/view/ObjectListTable.vue'
import TooltipButton from '@/components/widgets/TooltipButton'
import UpdateRegisteredExtension from '@/views/extension/UpdateRegisteredExtension'
export default {
name: 'ExtensionResourcesTab',
components: {
ObjectListTable,
TooltipButton
TooltipButton,
UpdateRegisteredExtension
},
inject: ['parentFetchData'],
props: {
resource: {
type: Object,
@ -103,7 +127,9 @@ export default {
title: this.$t('label.actions')
}
],
unregisterLoading: false
unregisterLoading: false,
updateModalVisible: false,
selectedResource: null
}
},
computed: {
@ -112,13 +138,27 @@ export default {
}
},
methods: {
openUpdateModal (record) {
this.selectedResource = record
this.updateModalVisible = true
},
closeUpdateModal () {
this.updateModalVisible = false
this.selectedResource = null
},
handleRefreshData () {
if (this.parentFetchData) {
this.parentFetchData()
}
},
unregisterExtension (record) {
const params = {
extensionid: this.resource.id,
resourceid: record.id,
resourcetype: record.type
}
postAPI('unregisterExtension', params).then(json => {
this.unregisterLoading = true
postAPI('unregisterExtension', params).then(() => {
eventBus.emit('async-job-complete', null)
this.$notification.success({
message: this.$t('label.unregister.extension'),
@ -127,7 +167,7 @@ export default {
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.deleteLoading = false
this.unregisterLoading = false
})
}
}

View File

@ -120,7 +120,7 @@ export default {
},
fetchExtensionResourceTypes () {
this.resourceTypes = []
const resourceTypesList = ['Cluster']
const resourceTypesList = ['Cluster', 'PhysicalNetwork']
resourceTypesList.forEach((item) => {
this.resourceTypes.push({
id: item,

View File

@ -0,0 +1,131 @@
// 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.
<template>
<div class="form-layout" v-ctrl-enter="handleSubmit">
<a-form
:ref="formRef"
:model="form"
:loading="loading"
layout="vertical"
@finish="handleSubmit">
<a-form-item name="details" ref="details">
<template #label>
<tooltip-label :title="$t('label.details')" :tooltip="$t('message.add.extension.resource.details')"/>
</template>
<div style="margin-bottom: 10px">{{ $t('message.add.extension.resource.details') }}</div>
<details-input
v-model:value="form.details" />
</a-form-item>
<div :span="24" class="action-button">
<a-button @click="closeAction">{{ $t('label.cancel') }}</a-button>
<a-button :loading="loading" ref="submit" type="primary" @click="handleSubmit">{{ $t('label.ok') }}</a-button>
</div>
</a-form>
</div>
</template>
<script>
import { ref, reactive, toRaw } from 'vue'
import { postAPI } from '@/api'
import TooltipLabel from '@/components/widgets/TooltipLabel'
import DetailsInput from '@/components/widgets/DetailsInput'
export default {
name: 'UpdateRegisteredExtension',
components: {
TooltipLabel,
DetailsInput
},
props: {
resource: {
type: Object,
required: true
},
extensionResource: {
type: Object,
required: true
}
},
data () {
return {
loading: false
}
},
created () {
this.initForm()
},
methods: {
initForm () {
this.formRef = ref()
this.form = reactive({
details: this.extensionResource.details ? { ...this.extensionResource.details } : {}
})
},
handleSubmit (e) {
e.preventDefault()
if (this.loading) return
this.formRef.value.validate().then(() => {
const values = toRaw(this.form)
this.loading = true
const params = {
extensionid: this.resource.id,
resourceid: this.extensionResource.id,
resourcetype: this.extensionResource.type
}
if (values.details && Object.keys(values.details).length > 0) {
Object.entries(values.details).forEach(([key, value]) => {
params['details[0].' + key] = value
})
} else {
params.cleanupdetails = true
}
postAPI('updateRegisteredExtension', params).then(() => {
this.$emit('refresh-data')
this.$notification.success({
message: this.$t('label.action.update.extension.resource'),
description: this.$t('message.success.update.extension.resource')
})
this.closeAction()
}).catch(error => {
this.$notification.error({
message: this.$t('message.request.failed'),
description: (error.response && error.response.headers && error.response.headers['x-description']) || error.message,
duration: 0
})
}).finally(() => {
this.loading = false
})
}).catch(error => {
this.$notifyError(error)
})
},
closeAction () {
this.$emit('close-action')
}
}
}
</script>
<style scoped lang="less">
.form-layout {
width: 80vw;
@media (min-width: 600px) {
width: 550px;
}
}
</style>

View File

@ -18,10 +18,21 @@
<template>
<div>
<a-spin :spinning="fetchLoading">
<!-- Add Extension Provider button: only shown when extension is registered but NSP not yet created -->
<a-button
v-if="isExtensionTab && !nsps[tabKey]"
:disabled="!('addNetworkServiceProvider' in $store.getters.apis)"
type="dashed"
style="width: 100%; margin-bottom: 12px;"
@click="handleAddExternalNetworkProvider">
<template #icon><plus-outlined /></template>
{{ $t('label.add.external.network.provider') }}
</a-button>
<a-tabs
:tabPosition="device === 'mobile' ? 'top' : 'left'"
:animated="false"
@change="onTabChange">
<!-- Hardcoded NSP tabs -->
<a-tab-pane
class="custom-tab-pane"
v-for="item in hardcodedNsps"
@ -41,8 +52,67 @@
:zoneId="resource.zoneid"
:tabKey="tabKey"/>
</a-tab-pane>
<!-- Dynamic extension-based provider tabs (one per registered NetworkOrchestrator extension) -->
<a-tab-pane
class="custom-tab-pane"
v-for="ext in registeredExtensions"
:key="ext.name">
<template #tab>
<span>
{{ ext.name }}
<status :text="nsps[ext.name] ? nsps[ext.name].state : $t('label.not.added')" style="margin-bottom: 6px; margin-left: 6px" />
</span>
</template>
<provider-item
v-if="tabKey === ext.name"
:loading="loading"
:itemNsp="extensionNspItem(ext.name)"
:nsp="nsps[ext.name]"
:resourceId="resource.id"
:zoneId="resource.zoneid"
:tabKey="tabKey"/>
</a-tab-pane>
</a-tabs>
</a-spin>
<!-- Add External Network Provider modal: selects extension (services come from extension capabilities) -->
<a-modal
:visible="showAddExtNetProviderModal"
:title="$t('label.add.external.network.provider')"
:maskClosable="false"
:footer="null"
@cancel="showAddExtNetProviderModal = false">
<a-spin :spinning="extensionProviderLoading">
<a-form layout="vertical">
<a-form-item :label="$t('label.extension')">
<a-select
v-model:value="extNetProviderForm.extensionId"
showSearch
optionFilterProp="label"
:filterOption="(input, option) => option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0">
<a-select-option
v-for="ext in availableExtensions"
:key="ext.id"
:value="ext.id"
:label="ext.name">
{{ ext.name }} <span style="color: #aaa">({{ ext.state }})</span>
</a-select-option>
</a-select>
<div v-if="availableExtensions.length === 0" style="color: #faad14; margin-top: 4px;">
{{ $t('message.no.network.orchestrator.extensions') }}
</div>
<div v-else style="color: #8c8c8c; font-size: 12px; margin-top: 4px;">
{{ $t('message.extension.services.from.capabilities') }}
</div>
</a-form-item>
<div class="action-button">
<a-button @click="showAddExtNetProviderModal = false">{{ $t('label.cancel') }}</a-button>
<a-button type="primary" :disabled="!extNetProviderForm.extensionId" @click="handleAddExtNetProvider">{{ $t('label.ok') }}</a-button>
</div>
</a-form>
</a-spin>
</a-modal>
<div v-if="showFormAction">
<keep-alive v-if="currentAction.component">
<a-modal
@ -170,10 +240,20 @@ export default {
actionLoading: false,
showFormAction: false,
currentAction: {},
tabKey: 'BaremetalDhcpProvider'
tabKey: 'BaremetalDhcpProvider',
showAddExtNetProviderModal: false,
extensionProviderLoading: false,
availableExtensions: [],
extNetProviderForm: {
extensionId: null
},
registeredExtensions: []
}
},
computed: {
isExtensionTab () {
return this.registeredExtensions.some(ext => ext.name === this.tabKey)
},
hardcodedNsps () {
return [
{
@ -848,7 +928,7 @@ export default {
listView: true,
label: 'label.enable.provider',
confirm: 'message.confirm.enable.provider',
show: (record) => { return (record && record.id && record.state === 'Disabled') },
show: (record) => { return record && record.id && record.state === 'Disabled' },
mapping: {
state: {
value: (record) => { return 'Enabled' }
@ -1143,11 +1223,106 @@ export default {
this.form = reactive({})
this.rules = reactive({})
},
handleAddExternalNetworkProvider () {
// Open the extension picker modal services come from extension capabilities
this.extNetProviderForm = { extensionId: null }
this.extensionProviderLoading = true
this.showAddExtNetProviderModal = true
getAPI('listExtensions', { type: 'NetworkOrchestrator' }).then(json => {
this.availableExtensions = (json.listextensionsresponse && json.listextensionsresponse.extension) || []
if (this.availableExtensions.length > 0) {
this.extNetProviderForm.extensionId = this.availableExtensions[0].id
}
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.extensionProviderLoading = false
})
},
_updateServicesFromExtension (extensionId) {
// No longer needed services are derived from extension capabilities server-side
},
async handleAddExtNetProvider () {
if (this.extensionProviderLoading) return
const extensionId = this.extNetProviderForm.extensionId
if (!extensionId) {
this.$message.error(this.$t('message.select.extension'))
return
}
const ext = this.availableExtensions.find(e => e.id === extensionId)
const extName = ext ? ext.name : ''
if (!extName) {
this.$message.error(this.$t('message.select.extension'))
return
}
this.extensionProviderLoading = true
try {
// registerExtension auto-creates the NSP (Enabled) with services from network.capabilities
await postAPI('registerExtension', {
extensionid: extensionId,
resourceid: this.resource.id,
resourcetype: 'PhysicalNetwork'
})
this.$message.success(this.$t('label.add.external.network.provider') + ': ' + extName)
this.showAddExtNetProviderModal = false
this.fetchData()
} catch (error) {
this.$notifyError(error)
} finally {
this.extensionProviderLoading = false
}
},
fetchData () {
if (!this.resource || !('id' in this.resource)) {
return
}
this.fetchServiceProvider()
this.fetchRegisteredExtensions()
},
fetchRegisteredExtensions () {
// Load NetworkOrchestrator extensions registered to this physical network
getAPI('listExtensions', {
type: 'NetworkOrchestrator',
resourceid: this.resource.id,
resourcetype: 'PhysicalNetwork'
}).then(json => {
this.registeredExtensions = (json.listextensionsresponse && json.listextensionsresponse.extension) || []
}).catch(() => {
this.registeredExtensions = []
})
},
extensionNspItem (extName) {
// Build a ProviderItem-compatible itemNsp descriptor for extension-backed NSPs.
// Mirrors the structure of hardcoded entries in hardcodedNsps.
return {
title: extName,
details: ['name', 'state', 'id', 'physicalnetworkid', 'servicelist'],
actions: [
{
api: 'updateNetworkServiceProvider',
icon: 'play-circle-outlined',
listView: true,
label: 'label.enable.provider',
confirm: 'message.confirm.enable.provider',
show: (record) => record && record.id && record.state === 'Disabled',
mapping: {
state: { value: () => 'Enabled' }
}
},
{
api: 'updateNetworkServiceProvider',
icon: 'stop-outlined',
listView: true,
label: 'label.disable.provider',
confirm: 'message.confirm.disable.provider',
show: (record) => record && record.id && record.state === 'Enabled',
mapping: {
state: { value: () => 'Disabled' }
}
}
]
}
},
fetchServiceProvider (name) {
this.fetchLoading = true

View File

@ -25,7 +25,7 @@
:loading="loading"
:columns="listCols"
:dataSource="dataSource"
:rowKey="record => record.id || record.name || record.nvpdeviceid || record.resourceid"
:rowKey="record => record.id || record.name || record.nvpdeviceid || record.resourceid || record.physicalnetworkid"
:pagination="false"
:scroll="scrollable">
<template #bodyCell="{ column, text, record }">
@ -67,6 +67,7 @@
<span v-else-if="resource.name==='CiscoVnmc' && title==='listCiscoAsa1000vResources'">
{{ $t('label.delete.ciscoasa1000v') }}
</span>
<!-- External network device UI removed: use extension registration details instead -->
</template>
<tooltip-button
v-if="resource.name==='Ovs'"
@ -86,6 +87,12 @@
@onClick="onDelete(record)"/>
</a-tooltip>
</template>
<template v-if="column.key === 'details'">
<span v-if="text && typeof text === 'object'">
<a-tag v-for="(val, key) in text" :key="key" style="margin-bottom: 2px;">{{ key }}: {{ val }}</a-tag>
</span>
<span v-else>{{ text }}</span>
</template>
<template v-if="column.key === 'lbdevicestate'">
<status :text="text ? text : ''" displayText />
</template>
@ -269,6 +276,7 @@ export default {
name = record.hostname
params.resourceid = record.resourceid
break
// ExternalNetwork provider action removed; use extension registration/unregister instead
default:
break
}

View File

@ -60,10 +60,10 @@
<a-radio-button value="isolated">
{{ $t('label.isolated') }}
</a-radio-button>
<a-radio-button value="l2" v-if="form.provider !== 'NSX' && form.provider !== 'Netris'">
<a-radio-button value="l2" v-if="form.provider !== 'NSX' && form.provider !== 'Netris' && !isExternalNetworkProvider">
{{ $t('label.l2') }}
</a-radio-button>
<a-radio-button value="shared" v-if="form.provider !== 'NSX' && form.provider !== 'Netris'">
<a-radio-button value="shared" v-if="form.provider !== 'NSX' && form.provider !== 'Netris' && !isExternalNetworkProvider">
{{ $t('label.shared') }}
</a-radio-button>
</a-radio-group>
@ -138,7 +138,16 @@
<a-select-option key="" >{{ }}</a-select-option>
<a-select-option :value="'NSX'" :label="$t('label.nsx')"> {{ $t('label.nsx') }} </a-select-option>
<a-select-option :value="'Netris'" :label="$t('label.netris')"> {{ $t('label.netris') }} </a-select-option>
</a-select>
<!-- Dynamic extension-based providers registered to guest physical networks.
The value is the extension/NSP name so the provider is correctly resolved. -->
<a-select-option
v-for="ext in availableExtensionProviders"
:key="ext.name"
:value="ext.name"
:label="ext.name">
{{ ext.name }} <span style="color: #aaa">({{ $t('label.external.network.provider') }})</span>
</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
@ -208,7 +217,7 @@
</a-form-item>
<a-row :gutter="12">
<a-col :md="12" :lg="12">
<a-form-item name="promiscuousmode" ref="promiscuousmode" v-if="form.provider !== 'NSX' && form.provider !== 'Netris'">
<a-form-item name="promiscuousmode" ref="promiscuousmode" v-if="form.provider !== 'NSX' && form.provider !== 'Netris' && !isExternalNetworkProvider">
<template #label>
<tooltip-label :title="$t('label.promiscuousmode')" :tooltip="$t('message.network.offering.promiscuous.mode')"/>
</template>
@ -303,8 +312,8 @@
<CheckBoxSelectPair
:resourceKey="item.name"
:checkBoxLabel="item.description"
:forExternalNetProvider="form.provider === 'NSX' || form.provider === 'Netris'"
:defaultCheckBoxValue="form.provider === 'NSX' || form.provider === 'Netris'"
:forExternalNetProvider="form.provider === 'NSX' || form.provider === 'Netris' || isExternalNetworkProvider"
:defaultCheckBoxValue="form.provider === 'NSX' || form.provider === 'Netris' || isExternalNetworkProvider"
:selectOptions="!supportedServiceLoading ? item.provider: []"
@handle-checkselectpair-change="handleSupportedServiceChange"/>
</a-list-item>
@ -669,8 +678,20 @@ export default {
description: 'Netris',
enabled: true
},
externalNetworkProviderObj: {
name: '',
description: 'External Network',
enabled: true
},
nsxSupportedServicesMap: {},
netrisSupportedServicesMap: {}
netrisSupportedServicesMap: {},
externalNetworkSupportedServicesMap: {},
availableExtensionProviders: []
}
},
computed: {
isExternalNetworkProvider () {
return this.availableExtensionProviders.some(e => e.name === this.provider)
}
},
beforeCreate () {
@ -732,6 +753,30 @@ export default {
this.fetchServiceOfferingData()
this.fetchIpv6NetworkOfferingConfiguration()
this.fetchRoutedNetworkConfiguration()
this.fetchExtensionProviders()
},
fetchExtensionProviders () {
// Load NetworkOrchestrator extensions that are registered to at least one
// physical network (i.e. have a corresponding NetworkServiceProvider entry).
// Only these can be selected as a provider when creating a network offering.
getAPI('listExtensions', { type: 'NetworkOrchestrator', state: 'Enabled' }).then(json => {
const allExts = (json.listextensionsresponse && json.listextensionsresponse.extension) || []
if (allExts.length === 0) {
this.availableExtensionProviders = []
return
}
// Filter to those which have at least one matching NSP (nsp name == extension name)
getAPI('listNetworkServiceProviders', {}).then(nspJson => {
const nsps = (nspJson.listnetworkserviceprovidersresponse && nspJson.listnetworkserviceprovidersresponse.networkserviceprovider) || []
const nspNames = new Set(nsps.map(n => n.name))
this.availableExtensionProviders = allExts.filter(e => nspNames.has(e.name))
}).catch(() => {
// Fallback: show all enabled extensions
this.availableExtensionProviders = allExts
})
}).catch(() => {
this.availableExtensionProviders = []
})
},
isAdmin () {
return isAdmin()
@ -906,7 +951,7 @@ export default {
this.supportedServiceLoading = true
var supportedServices = this.supportedServices
var self = this
if (this.provider !== 'NSX' && this.provider !== 'Netris') {
if (this.provider !== 'NSX' && this.provider !== 'Netris' && !this.isExternalNetworkProvider) {
if (this.networkmode === 'ROUTED' && this.guestType === 'isolated') {
supportedServices = supportedServices.filter(service => {
return !['SourceNat', 'StaticNat', 'Lb', 'PortForwarding', 'Vpn'].includes(service.name)
@ -943,6 +988,8 @@ export default {
return Object.keys(this.nsxSupportedServicesMap).includes(svc.name)
} else if (this.provider === 'Netris') {
return Object.keys(this.netrisSupportedServicesMap).includes(svc.name)
} else if (this.isExternalNetworkProvider) {
return Object.keys(this.externalNetworkSupportedServicesMap).includes(svc.name)
}
})
supportedServices = supportedServices.map(svc => {
@ -951,6 +998,8 @@ export default {
svc.provider = [this.NSX]
} else if (this.provider === 'Netris') {
svc.provider = [this.Netris]
} else if (this.isExternalNetworkProvider) {
svc.provider = [this.externalNetworkProviderObj]
}
} else {
if (this.forVpc) {
@ -1029,9 +1078,46 @@ export default {
...(this.forVpc && { NetworkACL: this.Netris }),
...(!this.forVpc && { Firewall: this.Netris })
}
} else if (this.isExternalNetworkProvider) {
// Extension-backed provider: services come from the extension's network.capabilities.
// this.provider is the extension name (= NSP name)
const extProviderObj = {
name: this.provider,
description: this.provider,
enabled: true
}
const svcMap = { Dhcp: this.VR, Dns: this.VR, UserData: this.VR }
// Infer services from the selected extension's network.capabilities detail
const extDef = this.availableExtensionProviders.find(e => e.name === this.provider)
const services = this._getExtensionServices(extDef)
if (services.length > 0) {
services.forEach(svc => {
if (!['Dhcp', 'Dns', 'UserData'].includes(svc)) {
svcMap[svc] = extProviderObj
}
})
} else {
// Default services if no capabilities declared
svcMap.SourceNat = extProviderObj
svcMap.StaticNat = extProviderObj
svcMap.PortForwarding = extProviderObj
svcMap.Firewall = extProviderObj
svcMap.Gateway = extProviderObj
}
this.externalNetworkSupportedServicesMap = svcMap
this.externalNetworkProviderObj = extProviderObj
}
this.fetchSupportedServiceData()
},
_getExtensionServices (extDef) {
if (!extDef || !extDef.details || !extDef.details['network.capabilities']) return []
try {
const caps = JSON.parse(extDef.details['network.capabilities'])
return (caps && caps.services) ? caps.services : []
} catch (e) {
return []
}
},
handleForNetworkModeChange (networkMode) {
this.networkmode = networkMode
this.fetchSupportedServiceData()

View File

@ -86,6 +86,15 @@
<a-select-option key="" >{{ }}</a-select-option>
<a-select-option :value="'NSX'" :label="$t('label.nsx')"> {{ $t('label.nsx') }} </a-select-option>
<a-select-option :value="'Netris'" :label="$t('label.netris')"> {{ $t('label.netris') }} </a-select-option>
<!-- Dynamic extension-based providers registered to guest physical networks.
The value is the extension/NSP name for correct provider resolution. -->
<a-select-option
v-for="ext in availableExtensionProviders"
:key="ext.name"
:value="ext.name"
:label="ext.name">
{{ ext.name }} <span style="color: #aaa">({{ $t('label.external.network.provider') }})</span>
</a-select-option>
</a-select>
</a-form-item>
</a-col>
@ -152,8 +161,8 @@
<CheckBoxSelectPair
:resourceKey="item.name"
:checkBoxLabel="item.description"
:forExternalNetProvider="form.provider === 'NSX' || form.provider === 'Netris'"
:defaultCheckBoxValue="form.provider === 'NSX' || form.provider === 'Netris'"
:forExternalNetProvider="form.provider === 'NSX' || form.provider === 'Netris' || isExternalNetworkProvider"
:defaultCheckBoxValue="form.provider === 'NSX' || form.provider === 'Netris' || isExternalNetworkProvider"
:selectOptions="item.provider"
@handle-checkselectpair-change="handleSupportedServiceChange"/>
</a-list-item>
@ -337,9 +346,22 @@ export default {
enabled: true
},
nsxSupportedServicesMap: {},
externalNetworkProviderObj: {
name: '',
description: 'External Network',
enabled: true
},
externalNetworkSupportedServicesMap: {},
availableExtensionProviders: [],
conservemode: false
}
},
computed: {
isExternalNetworkProvider () {
const selectedProvider = this.form?.provider || this.provider
return this.availableExtensionProviders.some(e => e.name === selectedProvider)
}
},
beforeCreate () {
this.apiParams = this.$getApiParams('createVPCOffering')
},
@ -384,6 +406,25 @@ export default {
this.fetchSupportedServiceData()
this.fetchIpv6NetworkOfferingConfiguration()
this.fetchRoutedNetworkConfiguration()
this.fetchExtensionProviders()
},
fetchExtensionProviders () {
getAPI('listExtensions', { type: 'NetworkOrchestrator', state: 'Enabled' }).then(json => {
const allExts = (json.listextensionsresponse && json.listextensionsresponse.extension) || []
if (allExts.length === 0) {
this.availableExtensionProviders = []
return
}
getAPI('listNetworkServiceProviders', {}).then(nspJson => {
const nsps = (nspJson.listnetworkserviceprovidersresponse && nspJson.listnetworkserviceprovidersresponse.networkserviceprovider) || []
const nspNames = new Set(nsps.map(n => n.name))
this.availableExtensionProviders = allExts.filter(e => nspNames.has(e.name))
}).catch(() => {
this.availableExtensionProviders = allExts
})
}).catch(() => {
this.availableExtensionProviders = []
})
},
isAdmin () {
return isAdmin()
@ -433,7 +474,18 @@ export default {
},
fetchSupportedServiceData () {
var services = []
if (this.provider === 'NSX') {
if (this.isExternalNetworkProvider) {
const serviceMap = this._buildExternalVpcServiceMap()
Object.keys(serviceMap).forEach(serviceName => {
services.push({
name: serviceName,
enabled: true,
provider: Array.isArray(serviceMap[serviceName])
? serviceMap[serviceName]
: [serviceMap[serviceName]]
})
})
} else if (this.provider === 'NSX') {
services.push({
name: 'Dhcp',
enabled: true,
@ -624,9 +676,76 @@ export default {
if (this.provider === 'NSX') {
this.form.nsxsupportlb = true
this.handleNsxLbService(true)
} else if (this.isExternalNetworkProvider) {
this._buildExternalVpcServiceMap()
}
this.fetchSupportedServiceData()
},
_getExtensionServices (extDef) {
if (!extDef || !extDef.details) {
return []
}
const capsJson = extDef.details['network.capabilities']
if (capsJson) {
try {
const caps = JSON.parse(capsJson)
if (caps && Array.isArray(caps.services)) {
return caps.services
}
} catch (e) {
// Ignore malformed capabilities and fallback to network.services.
}
}
const servicesCsv = extDef.details['network.services']
if (servicesCsv && typeof servicesCsv === 'string') {
return servicesCsv.split(',').map(x => x.trim()).filter(x => x.length > 0)
}
return []
},
_buildExternalVpcServiceMap () {
const selectedProvider = this.form?.provider || this.provider
const extProviderObj = {
name: selectedProvider,
description: selectedProvider,
enabled: true
}
const extWithFallbackProviders = [
{ name: selectedProvider },
{ name: 'VpcVirtualRouter' },
{ name: 'ConfigDrive' }
]
const serviceMap = {
Dhcp: extWithFallbackProviders,
Dns: extWithFallbackProviders,
UserData: extWithFallbackProviders
}
const extDef = this.availableExtensionProviders.find(e => e.name === selectedProvider)
const services = this._getExtensionServices(extDef)
const allowedVpcServices = new Set([
'Gateway', 'Lb', 'StaticNat', 'SourceNat', 'NetworkACL', 'PortForwarding', 'Vpn'
])
services.forEach(service => {
if (allowedVpcServices.has(service)) {
serviceMap[service] = [{ name: selectedProvider }]
}
})
// Fallback for older extensions that only declare partial details.
if (Object.keys(serviceMap).length <= 3) {
serviceMap.SourceNat = [{ name: selectedProvider }]
serviceMap.StaticNat = [{ name: selectedProvider }]
serviceMap.PortForwarding = [{ name: selectedProvider }]
serviceMap.NetworkACL = [{ name: selectedProvider }]
}
this.externalNetworkProviderObj = extProviderObj
this.externalNetworkSupportedServicesMap = serviceMap
return serviceMap
},
handleNsxLbService (supportLb) {
console.log(supportLb)
if (!supportLb) {