Network Extension: Orchestrate external Network devices (#13032)
Some checks are pending
CodeQL Analysis / CodeQL (actions) (push) Waiting to run
Docker Image Build / build (push) Waiting to run
Sonar Quality Check (Main) / Sonar JaCoCo Coverage (push) Waiting to run

This PR introduces and wires a new extension model for external network orchestration in CloudStack, centered on a new extension type: NetworkOrchestrator.

It extends the extension lifecycle from cluster-only registration to physical-network registration, adds API support for updating registered extension metadata, and enables automatic offering creation (network and VPC) based on provider-declared supported services and capabilities.

It also adds smoke coverage (KVM-only) using a Linux network namespace based implementation.

Doc PR: apache/cloudstack-documentation#649

What's new

1) New extension type: NetworkOrchestrator
Adds support for creating extensions of type NetworkOrchestrator.
Intended to back CloudStack network/VPC operations via an external orchestrator/provider.

2) Register extension with PhysicalNetwork (in addition to Cluster)
Extensions can now be registered against a PhysicalNetwork resource.
This enables network service provider behavior at physical-network scope, not only cluster scope.

3) Physical network registration details support
Registered extension details for PhysicalNetwork are handled similarly to cluster registration details.
Supports storing/updating external access metadata (credentials/endpoints/config details).

4) New API: update registered extension
Adds API support to update extension registration metadata after registration.
Useful for rotating credentials, updating endpoints, and changing external connection properties without re-registering.

5) Offering automation from external provider capabilities
Network/VPC offerings can be created with the external network provider using:
provider supportedservices
per-service service capabilities
This allows CloudStack offerings to align with what the external provider actually supports.

6) Network support via generated offerings
Using offerings backed by the external provider, networks can be created and operated with supported services/capabilities.

Supported operations include (based on provider capabilities):

Source NAT
Static NAT
Port Forwarding
Firewall
Load Balancing
DHCP
DNS
UserData

7) VPC support via generated offerings
Using VPC offerings backed by the external provider, VPCs and tiers can be created and operated with supported services/capabilities.

Supported operations include (based on provider capabilities):

VPC tier creation/implementation
Source NAT in VPC context
Static NAT / Port Forwarding / LB on VPC tiers
Network ACL association and ACL rule apply paths
Related lifecycle/restart/reapply operations

8) Linux network namespace based external implementation
Adds/uses a network extension implementation based on Linux network namespaces.
Reference implementation:
https://github.com/apache/cloudstack-extensions/tree/network-namespace/Network-Namespace

9) Smoke test coverage (KVM-only)
Adds smoke tests using the namespace-based extension implementation.
Scope includes provider lifecycle, offering creation, network/VPC flows, and key network services.
Applicable hypervisor for this smoke suite: KVM.
This commit is contained in:
Wei Zhou 2026-07-10 08:44:28 +02:00 committed by GitHub
parent 20e9980b60
commit 8996286a95
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
89 changed files with 12181 additions and 413 deletions

View File

@ -875,6 +875,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

@ -116,6 +116,7 @@ public interface Network extends ControlledEntity, StateObject<Network.State>, I
public static final Service NetworkACL = new Service("NetworkACL", Capability.SupportedProtocols);
public static final Service Connectivity = new Service("Connectivity", Capability.DistributedRouter, Capability.RegionLevelVpc, Capability.StretchedL2Subnet,
Capability.NoVlan, Capability.PublicAccess);
public static final Service CustomAction = new Service("CustomAction");
private final String name;
private final Capability[] caps;
@ -207,6 +208,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 +252,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

@ -81,7 +81,11 @@ public class Networks {
return uri == null ? null : uri.getAuthority();
}
},
Vswitch("vs", String.class), LinkLocal(null, null), Vnet("vnet", Long.class), Storage("storage", Integer.class), Lswitch("lswitch", String.class) {
Vswitch("vs", String.class),
LinkLocal(null, null),
Vnet("vnet", Long.class),
Storage("storage", Integer.class),
Lswitch("lswitch", String.class) {
@Override
public <T> URI toUri(T value) {
try {
@ -99,7 +103,8 @@ public class Networks {
return uri == null ? null : uri.getSchemeSpecificPart();
}
},
Mido("mido", String.class), Pvlan("pvlan", String.class),
Mido("mido", String.class),
Pvlan("pvlan", String.class),
Vxlan("vxlan", Long.class) {
@Override
public <T> URI toUri(T value) {

View File

@ -33,4 +33,6 @@ public interface DnsServiceProvider extends NetworkElement {
throws ConcurrentOperationException, InsufficientCapacityException, ResourceUnavailableException;
boolean removeDnsSupportForSubnet(Network network) throws ResourceUnavailableException;
default boolean removeDnsEntry(Network network, NicProfile nic, VirtualMachineProfile vmProfile) throws ResourceUnavailableException { return true; }
}

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

@ -463,6 +463,6 @@ public class NicProfile implements InternalIdentity, Serializable {
return String.format("NicProfile %s",
ReflectionToStringBuilderUtils.reflectOnlySelectedFields(
this, "id", "uuid", "vmId", "deviceId",
"broadcastUri", "reservationId", "iPv4Address"));
"broadcastType", "broadcastUri", "reservationId", "iPv4Address"));
}
}

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,9 @@ 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),
Vpc(com.cloud.network.vpc.Vpc.class);
private final Class<?> clazz;

View File

@ -18,10 +18,125 @@
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 getExtensionByNameAndType(String name, Extension.Type type);
Extension getExtensionForCluster(long clusterId);
List<String> getExtensionReservedResourceDetails(long extensionId);
/**
* Network Guru of Network Extension
*/
String NETWORK_EXTENSION_GURU_NAME = "NetworkExtensionGuestNetworkGuru";
/**
* Extension resource-map detail key holding the isolation method for the physical
* network (e.g. {@code "NetworkExtension"}).
* If not specified, the isolation method of physical network will be used.
*/
String NETWORK_ISOLATION_METHOD_DETAIL_KEY = "network.isolation.method";
/**
* Value of {@link #NETWORK_ISOLATION_METHOD_DETAIL_KEY} that indicates
* the extension owns the network isolation.
*/
String NETWORK_EXTENSION_ISOLATION_METHOD = "NetworkExtension";
/**
* 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";
String getExtensionScriptPath(Extension extension);
/**
* 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 {@code true} when the extension registered for {@code providerName} on
* the given physical network has its {@link #NETWORK_ISOLATION_METHOD_DETAIL_KEY}
* resource-map detail set to {@link #NETWORK_EXTENSION_ISOLATION_METHOD}.
*
* @param providerName provider / extension name
* @return true if the extension uses NetworkExtension isolation
*/
boolean usesNetworkExtensionIsolation(String providerName);
/**
* 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,74 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.extension;
import java.util.Map;
import com.cloud.network.Network;
import com.cloud.network.vpc.Vpc;
/**
* Implemented by network elements that support running custom actions on a
* managed network or VPC (e.g. NetworkExtensionElement).
*
* <p>This interface is looked up by {@code ExtensionsManagerImpl} to dispatch
* {@code runCustomAction} requests whose resource type is {@code Network}
* or {@code Vpc}.</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);
/**
* Returns {@code true} if this provider can handle custom actions for
* the given VPC.
*
* @param vpc the target VPC
* @return {@code true} if this provider can handle the VPC
*/
boolean canHandleVpcCustomAction(Vpc vpc);
/**
* Runs a named custom action against the external network device that
* manages the given network.
*
* @param network the CloudStack network on which to run the action
* @param actionName the action name (e.g. {@code "reboot-device"}, {@code "dump-config"})
* @param parameters optional parameters supplied by the caller
* @return output from the action script, or {@code null} on failure
*/
String runCustomAction(Network network, String actionName, Map<String, Object> parameters);
/**
* Runs a named custom action against the external network device that
* manages the given VPC.
*
* @param vpc the CloudStack VPC on which to run the action
* @param actionName the action name
* @param parameters optional parameters supplied by the caller
* @return output from the action script, or {@code null} on failure
*/
String runCustomAction(Vpc vpc, String actionName, Map<String, Object> parameters);
}

View File

@ -0,0 +1,73 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package com.cloud.network;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
public class NetworkTest {
@Test
public void testProviderContains() {
List<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));
}
@Test
public void testCustomActionServiceLookup() {
Network.Service customAction = Network.Service.getService("CustomAction");
assertNotNull("CustomAction service should be available", customAction);
assertTrue("CustomAction should be part of the supported services list",
Network.Service.listAllServices().contains(customAction));
}
@Test
public void testTransientProviderIsNotGloballyRegistered() {
Network.Provider transientProvider = Network.Provider.createTransientProvider("TransientOnly");
assertNotNull(transientProvider);
assertNull("Transient provider should not be retrievable from the global registry",
Network.Provider.getProvider("TransientOnly"));
}
}

View File

@ -0,0 +1,92 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.extension;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
public class CustomActionResultResponseTest {
private CustomActionResultResponse response;
@Before
public void setUp() {
response = new CustomActionResultResponse();
}
@Test
public void getResultReturnsNullByDefault() {
assertNull(response.getResult());
}
@Test
public void getResultReturnsSetValue() {
Map<String, String> result = Map.of("message", "OK", "details", "All good");
response.setResult(result);
assertEquals(result, response.getResult());
assertEquals("OK", response.getResult().get("message"));
}
@Test
public void isSuccessReturnsFalseWhenSuccessIsNull() {
// success is null by default
assertFalse(response.isSuccess());
}
@Test
public void isSuccessReturnsFalseWhenSuccessIsFalse() {
response.setSuccess(false);
assertFalse(response.isSuccess());
}
@Test
public void isSuccessReturnsTrueWhenSuccessIsTrue() {
response.setSuccess(true);
assertTrue(response.isSuccess());
}
@Test
public void getSuccessReturnsNullByDefault() {
assertNull(response.getSuccess());
}
@Test
public void getSuccessReturnsTrueAfterSetSuccessTrue() {
response.setSuccess(true);
assertTrue(response.getSuccess());
}
@Test
public void getSuccessReturnsFalseAfterSetSuccessFalse() {
response.setSuccess(false);
assertFalse(response.getSuccess());
}
@Test
public void setAndGetResultWithNullResult() {
response.setResult(null);
assertNull(response.getResult());
}
}

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

@ -142,6 +142,8 @@ public interface NetworkOrchestrationService {
boolean errorIfAlreadySetup, Long domainId, ACLType aclType, Boolean subdomainAccess, Long vpcId, Boolean isDisplayNetworkEnabled)
throws ConcurrentOperationException;
boolean isIsolationMethodNetworkExtension(Long networkOfferingId);
void allocate(VirtualMachineProfile vm, LinkedHashMap<? extends Network, List<? extends NicProfile>> networks, Map<String, Map<Integer, String>> extraDhcpOptions) throws InsufficientCapacityException,
ConcurrentOperationException;

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;
@ -313,6 +317,8 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra
@Inject
NetworkOfferingDetailsDao _ntwkOffDetailsDao;
@Inject
NetworkOfferingServiceMapDao networkOfferingServiceMapDao;
@Inject
AccountGuestVlanMapDao _accountGuestVlanMapDao;
@Inject
DataCenterVnetDao _datacenterVnetDao;
@ -368,6 +374,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 +471,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>
*/
protected List<NetworkElement> getNetworkElementsIncludingExtensions() {
List<Extension> extensions = extensionHelper.listExtensionsByType(Extension.Type.NetworkOrchestrator);
if (CollectionUtils.isEmpty(extensions)) {
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;
@ -776,7 +808,7 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra
long related = -1;
for (final NetworkGuru guru : networkGurus) {
for (final NetworkGuru guru : getDesignNetworkGurus(offering)) {
final Network network = guru.design(offering, plan, predefined, name, vpcId, owner);
if (network == null) {
continue;
@ -852,6 +884,46 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra
}
}
private List<NetworkGuru> getDesignNetworkGurus(final NetworkOffering offering) {
if (!isIsolationMethodNetworkExtension(offering.getId())) {
return networkGurus;
}
List<NetworkGuru> extensionGurus = networkGurus.stream()
.filter(guru -> ExtensionHelper.NETWORK_EXTENSION_GURU_NAME.equals(guru.getName()))
.collect(Collectors.toList());
if (CollectionUtils.isNotEmpty(extensionGurus)) {
return extensionGurus;
}
logger.warn("Network offering {} requests {}={}, but {} is not registered; using default guru ordering",
offering.getUuid(), ExtensionHelper.NETWORK_ISOLATION_METHOD_DETAIL_KEY,
ExtensionHelper.NETWORK_EXTENSION_ISOLATION_METHOD, ExtensionHelper.NETWORK_EXTENSION_GURU_NAME);
return networkGurus;
}
@Override
public boolean isIsolationMethodNetworkExtension(final Long networkOfferingId) {
if (networkOfferingId == null) {
return false;
}
List<String> providers = networkOfferingServiceMapDao.getDistinctProviders(networkOfferingId);
if (CollectionUtils.isEmpty(providers)) {
return false;
}
for (String providerName : providers) {
if (!extensionHelper.isNetworkExtensionProvider(providerName)) {
continue;
}
if (extensionHelper.usesNetworkExtensionIsolation(providerName)) {
return true;
}
}
return false;
}
@NotNull
private static NetworkVO getNetworkVO(long id, final NetworkOffering offering, final DeploymentPlan plan, final Network predefined,
Network network, final NetworkGuru guru, final Account owner,
@ -1593,6 +1665,9 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra
// implement network elements and re-apply all the network rules
implementNetworkElementsAndResources(dest, context, network, offering);
// reload network after implementing the network
network = _networksDao.findById(networkId);
long dcId = dest.getDataCenter().getId();
if (networkMeetsPersistenceCriteria(network, offering, false)) {
setupPersistentNetwork(network, offering, dcId);
@ -1686,7 +1761,8 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra
}
}
for (final NetworkElement element : networkElements) {
List<NetworkElement> allNetworkElements = getNetworkElementsIncludingExtensions();
for (final NetworkElement element : allNetworkElements) {
if (element instanceof AggregatedCommandExecutor && providersToImplement.contains(element.getProvider())) {
((AggregatedCommandExecutor) element).prepareAggregatedExecution(network, dest);
}
@ -1703,7 +1779,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 : allNetworkElements) {
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 +1793,7 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra
}
reconfigureAndApplyStaticRouteForVpcVpn(network);
} finally {
for (final NetworkElement element : networkElements) {
for (final NetworkElement element : allNetworkElements) {
if (element instanceof AggregatedCommandExecutor && providersToImplement.contains(element.getProvider())) {
((AggregatedCommandExecutor) element).cleanupAggregatedExecution(network, dest);
}
@ -1738,7 +1814,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 +2107,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 +2120,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 +2151,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 +2178,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 +2306,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 +2361,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 +2405,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 +2487,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 +2523,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 +2610,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 +2673,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 +3404,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 +3515,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 +3886,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: "
@ -3827,6 +3903,17 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra
}
}
}
if (vmProfile.getType() == Type.User && element.getProvider() != null) {
if (_networkModel.areServicesSupportedInNetwork(network.getId(), Service.Dns)
&& _networkModel.isProviderSupportServiceInNetwork(network.getId(), Service.Dns, element.getProvider()) && element instanceof DnsServiceProvider) {
final DnsServiceProvider sp = (DnsServiceProvider) element;
try {
sp.removeDnsEntry(network, nicProfile, vmProfile);
} catch (ResourceUnavailableException e) {
logger.error("Failed to remove dns entry due to: ", e);
}
}
}
}
}
}
@ -3846,7 +3933,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 +3981,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 +4556,12 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra
if (provider == null) {
provider = _networkModel.getDefaultUniqueProviderForService(service).getName();
} else {
final Provider resolvedProvider = _networkModel.resolveProvider(provider);
if (resolvedProvider == null) {
throw new InvalidParameterValueException("Invalid provider " + provider + " configured for service " + service);
}
provider = resolvedProvider.getName();
}
// check that provider is supported
@ -4480,7 +4587,10 @@ 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));
final Provider provider = _networkModel.resolveProvider(providerName);
if (provider != null) {
providers.add(provider);
}
}
return providers;
@ -4646,7 +4756,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 +5041,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(vmIds)) {
return;
}
for (NetworkElement element : networkElements) {
for (NetworkElement element : getNetworkElementsIncludingExtensions()) {
if (element instanceof LoadBalancingServiceProvider) {
LoadBalancingServiceProvider lbProvider = (LoadBalancingServiceProvider)element;
lbProvider.expungeLbVmRefs(vmIds, batchSize);

View File

@ -27,6 +27,7 @@ import static org.mockito.Mockito.when;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -35,11 +36,15 @@ import com.cloud.dc.DataCenter;
import com.cloud.exception.InsufficientVirtualNetworkCapacityException;
import com.cloud.network.IpAddressManager;
import com.cloud.utils.Pair;
import org.apache.cloudstack.extension.Extension;
import org.apache.cloudstack.extension.ExtensionHelper;
import org.apache.cloudstack.framework.extensions.network.NetworkExtensionElement;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.springframework.test.util.ReflectionTestUtils;
import org.mockito.ArgumentMatchers;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
@ -68,6 +73,7 @@ import com.cloud.network.dao.NetworkVO;
import com.cloud.network.dao.PhysicalNetworkVO;
import com.cloud.network.dao.RouterNetworkDao;
import com.cloud.network.element.DhcpServiceProvider;
import com.cloud.network.element.NetworkElement;
import com.cloud.network.guru.GuestNetworkGuru;
import com.cloud.network.guru.NetworkGuru;
import com.cloud.network.vpc.VpcManager;
@ -105,6 +111,7 @@ public class NetworkOrchestratorTest extends TestCase {
private String guruName = "GuestNetworkGuru";
private String dhcpProvider = "VirtualRouter";
private NetworkGuru guru = mock(NetworkGuru.class);
private NetworkExtensionElement networkExtensionElement;
NetworkOfferingVO networkOffering = mock(NetworkOfferingVO.class);
@ -135,6 +142,9 @@ public class NetworkOrchestratorTest extends TestCase {
testOrchestrator.routerJoinDao = mock(DomainRouterJoinDao.class);
testOrchestrator._ipAddrMgr = mock(IpAddressManager.class);
testOrchestrator._entityMgr = mock(EntityManager.class);
testOrchestrator.extensionHelper = mock(ExtensionHelper.class);
networkExtensionElement = mock(NetworkExtensionElement.class);
ReflectionTestUtils.setField(testOrchestrator, "networkExtensionElement", networkExtensionElement);
DhcpServiceProvider provider = mock(DhcpServiceProvider.class);
Map<Network.Capability, String> capabilities = new HashMap<Network.Capability, String>();
@ -388,7 +398,7 @@ public class NetworkOrchestratorTest extends TestCase {
when(testOrchestrator._ipAddressDao.findByIpAndSourceNetworkId(Mockito.anyLong(), Mockito.anyString())).thenReturn(ipVoSpy);
}
VlanVO vlanSpy = Mockito.spy(new VlanVO(Vlan.VlanType.DirectAttached, "vlanTag", vlanGateway, vlanNetmask, 0l, "192.168.100.100 - 192.168.100.200", 0l, new Long(0l),
VlanVO vlanSpy = Mockito.spy(new VlanVO(Vlan.VlanType.DirectAttached, "vlanTag", vlanGateway, vlanNetmask, 0l, "192.168.100.100 - 192.168.100.200", 0l, 0l,
"ip6Gateway", "ip6Cidr", "ip6Range"));
Mockito.doReturn(0l).when(vlanSpy).getId();
@ -1010,4 +1020,57 @@ public class NetworkOrchestratorTest extends TestCase {
assertEquals("testtag", nicProfile.getName());
}
}
// -----------------------------------------------------------------------
// Tests for getNetworkElementsIncludingExtensions
// -----------------------------------------------------------------------
@Test
public void getNetworkElementsIncludingExtensionsReturnsBaseListWhenNoExtensions() {
when(testOrchestrator.extensionHelper.listExtensionsByType(Extension.Type.NetworkOrchestrator))
.thenReturn(Collections.emptyList());
DhcpServiceProvider dhcpProvider = mock(DhcpServiceProvider.class);
List<NetworkElement> elements = new ArrayList<>(List.of(dhcpProvider));
testOrchestrator.networkElements = elements;
List<NetworkElement> result = testOrchestrator.getNetworkElementsIncludingExtensions();
assertNotNull(result);
assertEquals(elements.size(), result.size());
}
@Test
public void getNetworkElementsIncludingExtensionsAddsExtensionElements() {
Extension ext = mock(Extension.class);
when(ext.getName()).thenReturn("my-net-ext");
when(testOrchestrator.extensionHelper.listExtensionsByType(Extension.Type.NetworkOrchestrator))
.thenReturn(List.of(ext));
NetworkExtensionElement extElement = mock(NetworkExtensionElement.class);
when(networkExtensionElement.withProviderName("my-net-ext")).thenReturn(extElement);
DhcpServiceProvider dhcpProvider = mock(DhcpServiceProvider.class);
testOrchestrator.networkElements = new ArrayList<>(List.of(dhcpProvider));
List<NetworkElement> result = testOrchestrator.getNetworkElementsIncludingExtensions();
assertNotNull(result);
assertEquals(2, result.size());
assertTrue(result.contains(extElement));
}
@Test
public void getNetworkElementsIncludingExtensionsReturnsBaseListWhenExtensionHelperReturnsNull() {
when(testOrchestrator.extensionHelper.listExtensionsByType(Extension.Type.NetworkOrchestrator))
.thenReturn(null);
DhcpServiceProvider dhcpProvider = mock(DhcpServiceProvider.class);
testOrchestrator.networkElements = new ArrayList<>(List.of(dhcpProvider));
List<NetworkElement> result = testOrchestrator.getNetworkElementsIncludingExtensions();
assertNotNull(result);
assertEquals(1, result.size());
}
}

View File

@ -38,7 +38,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;
@ -392,7 +391,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

@ -97,6 +97,9 @@ public class PhysicalNetworkServiceProviderVO implements PhysicalNetworkServiceP
@Column(name = "networkacl_service_provided")
boolean networkAclServiceProvided;
@Column(name = "custom_action_service_provided")
boolean customActionServiceProvided;
@Column(name = GenericDao.REMOVED_COLUMN)
Date removed;
@ -278,6 +281,7 @@ public class PhysicalNetworkServiceProviderVO implements PhysicalNetworkServiceP
this.setUserdataServiceProvided(services.contains(Service.UserData));
this.setSecuritygroupServiceProvided(services.contains(Service.SecurityGroup));
this.setNetworkAclServiceProvided(services.contains(Service.NetworkACL));
this.setCustomActionServiceProvided(services.contains(Service.CustomAction));
}
@Override
@ -301,21 +305,27 @@ public class PhysicalNetworkServiceProviderVO implements PhysicalNetworkServiceP
if (this.isLbServiceProvided()) {
services.add(Service.Lb);
}
if (this.sourcenatServiceProvided) {
if (this.isSourcenatServiceProvided()) {
services.add(Service.SourceNat);
}
if (this.staticnatServiceProvided) {
if (this.isStaticnatServiceProvided()) {
services.add(Service.StaticNat);
}
if (this.portForwardingServiceProvided) {
if (this.isPortForwardingServiceProvided()) {
services.add(Service.PortForwarding);
}
if (this.isNetworkAclServiceProvided()) {
services.add(Service.NetworkACL);
}
if (this.isUserdataServiceProvided()) {
services.add(Service.UserData);
}
if (this.isSecuritygroupServiceProvided()) {
services.add(Service.SecurityGroup);
}
if (this.isCustomActionServiceProvided()) {
services.add(Service.CustomAction);
}
return services;
}
@ -327,4 +337,12 @@ public class PhysicalNetworkServiceProviderVO implements PhysicalNetworkServiceP
public void setNetworkAclServiceProvided(boolean networkAclServiceProvided) {
this.networkAclServiceProvided = networkAclServiceProvided;
}
public boolean isCustomActionServiceProvided() {
return customActionServiceProvided;
}
public void setCustomActionServiceProvided(boolean customActionServiceProvided) {
this.customActionServiceProvided = customActionServiceProvided;
}
}

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

@ -42,4 +42,6 @@ public interface VpcOfferingServiceMapDao extends GenericDao<VpcOfferingServiceM
List<VpcOfferingServiceMapVO> listProvidersForServiceForVpcOffering(long vpcOfferingId, Service service);
List<Long> listOfferingIdsByServiceAndProvider(Service service, String provider);
}

View File

@ -37,6 +37,7 @@ public class VpcOfferingServiceMapDaoImpl extends GenericDaoBase<VpcOfferingServ
final SearchBuilder<VpcOfferingServiceMapVO> AllFieldsSearch;
final SearchBuilder<VpcOfferingServiceMapVO> MultipleServicesSearch;
final GenericSearchBuilder<VpcOfferingServiceMapVO, String> ServicesSearch;
final GenericSearchBuilder<VpcOfferingServiceMapVO, Long> OfferingIdsByServiceAndProviderSearch;
protected VpcOfferingServiceMapDaoImpl() {
super();
@ -56,6 +57,12 @@ public class VpcOfferingServiceMapDaoImpl extends GenericDaoBase<VpcOfferingServ
ServicesSearch.and("offeringId", ServicesSearch.entity().getVpcOfferingId(), SearchCriteria.Op.EQ);
ServicesSearch.select(null, Func.DISTINCT, ServicesSearch.entity().getService());
ServicesSearch.done();
OfferingIdsByServiceAndProviderSearch = createSearchBuilder(Long.class);
OfferingIdsByServiceAndProviderSearch.selectFields(OfferingIdsByServiceAndProviderSearch.entity().getVpcOfferingId());
OfferingIdsByServiceAndProviderSearch.and("service", OfferingIdsByServiceAndProviderSearch.entity().getService(), SearchCriteria.Op.EQ);
OfferingIdsByServiceAndProviderSearch.and("provider", OfferingIdsByServiceAndProviderSearch.entity().getProvider(), SearchCriteria.Op.EQ);
OfferingIdsByServiceAndProviderSearch.done();
}
@Override
@ -129,4 +136,12 @@ public class VpcOfferingServiceMapDaoImpl extends GenericDaoBase<VpcOfferingServ
return customSearch(sc, null);
}
@Override
public List<Long> listOfferingIdsByServiceAndProvider(Service service, String provider) {
SearchCriteria<Long> sc = OfferingIdsByServiceAndProviderSearch.create();
sc.setParameters("service", service.getName());
sc.setParameters("provider", provider);
return customSearch(sc, null);
}
}

View File

@ -44,4 +44,6 @@ public interface NetworkOfferingServiceMapDao extends GenericDao<NetworkOffering
List<String> listServicesForNetworkOffering(long networkOfferingId);
List<String> getDistinctProviders(long offId);
List<Long> listOfferingIdsByServiceAndProvider(Service service, String provider);
}

View File

@ -40,6 +40,7 @@ public class NetworkOfferingServiceMapDaoImpl extends GenericDaoBase<NetworkOffe
final GenericSearchBuilder<NetworkOfferingServiceMapVO, String> ProvidersSearch;
final GenericSearchBuilder<NetworkOfferingServiceMapVO, String> ServicesSearch;
final GenericSearchBuilder<NetworkOfferingServiceMapVO, String> DistinctProvidersSearch;
final GenericSearchBuilder<NetworkOfferingServiceMapVO, Long> OfferingIdsByServiceAndProviderSearch;
protected NetworkOfferingServiceMapDaoImpl() {
super();
@ -71,6 +72,12 @@ public class NetworkOfferingServiceMapDaoImpl extends GenericDaoBase<NetworkOffe
DistinctProvidersSearch.and("provider", DistinctProvidersSearch.entity().getProvider(), SearchCriteria.Op.EQ);
DistinctProvidersSearch.selectFields(DistinctProvidersSearch.entity().getProvider());
DistinctProvidersSearch.done();
OfferingIdsByServiceAndProviderSearch = createSearchBuilder(Long.class);
OfferingIdsByServiceAndProviderSearch.selectFields(OfferingIdsByServiceAndProviderSearch.entity().getNetworkOfferingId());
OfferingIdsByServiceAndProviderSearch.and("service", OfferingIdsByServiceAndProviderSearch.entity().getService(), SearchCriteria.Op.EQ);
OfferingIdsByServiceAndProviderSearch.and("provider", OfferingIdsByServiceAndProviderSearch.entity().getProvider(), SearchCriteria.Op.EQ);
OfferingIdsByServiceAndProviderSearch.done();
}
@Override
@ -170,4 +177,12 @@ public class NetworkOfferingServiceMapDaoImpl extends GenericDaoBase<NetworkOffe
List<String> results = customSearch(sc, null);
return results;
}
@Override
public List<Long> listOfferingIdsByServiceAndProvider(Service service, String provider) {
SearchCriteria<Long> sc = OfferingIdsByServiceAndProviderSearch.create();
sc.setParameters("service", service.getName());
sc.setParameters("provider", provider);
return customSearch(sc, null);
}
}

View File

@ -470,6 +470,13 @@ SELECT uuid(), role_id, 'quotaResourceStatement', permission, sort_order
FROM cloud.role_permissions rp
WHERE rule = 'quotaStatement' AND NOT EXISTS(SELECT 1 FROM cloud.role_permissions rp_ WHERE rp.role_id = rp_.role_id AND rp_.rule = 'quotaResourceStatement');
-- Increase length of value of extension details from 255 to 4096 to support longer details value
CALL `cloud`.`IDEMPOTENT_CHANGE_COLUMN`('cloud.extension_details', 'value', 'value', 'VARCHAR(4096)');
CALL `cloud`.`IDEMPOTENT_CHANGE_COLUMN`('cloud.extension_resource_map_details', 'value', 'value', 'VARCHAR(4096)');
-- Add CustomAction service support to physical_network_service_providers
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.physical_network_service_providers', 'custom_action_service_provided', 'tinyint(1) unsigned NOT NULL DEFAULT 0 COMMENT "Is Custom Action service provided" AFTER `networkacl_service_provided`');
--- Gui theme login base domain
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.gui_themes', 'login_base_domain', 'TEXT DEFAULT NULL');

View File

@ -70,6 +70,11 @@ 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)",
since = "4.23.0")
private String type;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
@ -82,6 +87,10 @@ public class ListExtensionsCmd extends BaseListCmd {
return extensionId;
}
public String getType() {
return type;
}
public EnumSet<ApiConstants.ExtensionDetails> getDetails() throws InvalidParameterValueException {
if (CollectionUtils.isEmpty(details)) {
return EnumSet.of(ApiConstants.ExtensionDetails.all);

View File

@ -0,0 +1,116 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.framework.extensions.api;
import java.util.EnumSet;
import java.util.Map;
import javax.inject.Inject;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.ExtensionResponse;
import org.apache.cloudstack.extension.Extension;
import org.apache.cloudstack.framework.extensions.manager.ExtensionsManager;
import com.cloud.user.Account;
@APICommand(name = "updateRegisteredExtension",
description = "Update details for an extension registered with a resource",
responseObject = ExtensionResponse.class,
responseHasSensitiveInfo = false,
entityType = {Extension.class},
authorized = {RoleType.Admin},
since = "4.23.0")
public class UpdateRegisteredExtensionCmd extends BaseCmd {
@Inject
ExtensionsManager extensionsManager;
@Parameter(name = ApiConstants.EXTENSION_ID, type = CommandType.UUID, required = true,
entityType = ExtensionResponse.class, description = "ID of the extension")
private Long extensionId;
@Parameter(name = ApiConstants.RESOURCE_ID, type = CommandType.STRING, required = true,
description = "ID of the resource where the extension is registered")
private String resourceId;
@Parameter(name = ApiConstants.RESOURCE_TYPE, type = CommandType.STRING, required = true,
description = "Type of the resource")
private String resourceType;
@Parameter(name = ApiConstants.DETAILS, type = CommandType.MAP,
description = "Details in key/value pairs using format details[i].keyname=keyvalue. Example: details[0].endpoint.url=urlvalue")
protected Map details;
@Parameter(name = ApiConstants.CLEAN_UP_DETAILS,
type = CommandType.BOOLEAN,
description = "Optional boolean field, which indicates if details should be cleaned up or not " +
"(If set to true, details removed for this registration, details field ignored; " +
"if false or not set, details can be updated through details map)")
private Boolean cleanupDetails;
public Long getExtensionId() {
return extensionId;
}
public String getResourceId() {
return resourceId;
}
public String getResourceType() {
return resourceType;
}
public Map<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,8 @@ import com.cloud.utils.db.GenericDao;
public interface ExtensionDao extends GenericDao<ExtensionVO, Long> {
ExtensionVO findByName(String name);
List<ExtensionVO> listByType(Extension.Type type);
ExtensionVO findByNameAndType(String name, 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,21 @@ 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);
}
@Override
public ExtensionVO findByNameAndType(String name, Extension.Type type) {
SearchCriteria<ExtensionVO> sc = AllFieldSearch.create();
sc.setParameters("name", name);
sc.setParameters("type", type);
return findOneBy(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);
ExtensionResourceMapVO findResourceByExtensionIdAndResourceIdAndType(long extensionId, long resourceId, ExtensionResourceMap.ResourceType resourceType);
List<Long> listResourceIdsByExtensionIdAndType(long extensionId, ExtensionResourceMap.ResourceType resourceType);
List<Long> listResourceIdsByType(ExtensionResourceMap.ResourceType resourceType);
}

View File

@ -55,6 +55,17 @@ public class ExtensionResourceMapDaoImpl extends GenericDaoBase<ExtensionResourc
return findOneBy(sc);
}
@Override
public ExtensionResourceMapVO findResourceByExtensionIdAndResourceIdAndType(long extensionId, long resourceId,
ExtensionResourceMap.ResourceType resourceType) {
SearchCriteria<ExtensionResourceMapVO> sc = genericSearch.create();
sc.setParameters("extensionId", extensionId);
sc.setParameters("resourceId", resourceId);
sc.setParameters("resourceType", resourceType);
return findOneBy(sc);
}
@Override
public List<Long> listResourceIdsByExtensionIdAndType(long extensionId, ExtensionResourceMap.ResourceType resourceType) {
GenericSearchBuilder<ExtensionResourceMapVO, Long> sb = createSearchBuilder(Long.class);
@ -67,4 +78,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

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

@ -18,6 +18,7 @@
package org.apache.cloudstack.framework.extensions.api;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import java.util.Arrays;
@ -89,4 +90,21 @@ public class ListExtensionsCmdTest {
setPrivateField("details", detailsList);
cmd.getDetails();
}
// -----------------------------------------------------------------------
// Tests for type getter
// -----------------------------------------------------------------------
@Test
public void testGetTypeReturnsValueWhenSet() {
setPrivateField("type", "NetworkOrchestrator");
assertEquals("NetworkOrchestrator", cmd.getType());
}
@Test
public void testGetTypeReturnsNullWhenUnset() {
setPrivateField("type", null);
assertNull(cmd.getType());
}
}

View File

@ -0,0 +1,160 @@
// 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.assertFalse;
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.ApiCommandResourceType;
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;
import com.cloud.user.Account;
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 extensionIdReturnsValueWhenSet() {
Long extensionId = 42L;
ReflectionTestUtils.setField(cmd, "extensionId", extensionId);
assertEquals(extensionId, cmd.getExtensionId());
}
@Test
public void cleanupDetailsReturnsNullWhenUnset() {
ReflectionTestUtils.setField(cmd, "cleanupDetails", null);
assertNull(cmd.isCleanupDetails());
}
@Test
public void cleanupDetailsReturnsTrueWhenSet() {
ReflectionTestUtils.setField(cmd, "cleanupDetails", true);
assertTrue(cmd.isCleanupDetails());
}
@Test
public void cleanupDetailsReturnsFalseWhenSetToFalse() {
ReflectionTestUtils.setField(cmd, "cleanupDetails", false);
assertFalse(cmd.isCleanupDetails());
}
@Test
public void getEntityOwnerIdReturnsSystemAccountId() {
assertEquals(Account.ACCOUNT_ID_SYSTEM, cmd.getEntityOwnerId());
}
@Test
public void getApiResourceTypeReturnsExtension() {
assertEquals(ApiCommandResourceType.Extension, cmd.getApiResourceType());
}
@Test
public void getApiResourceIdReturnsExtensionId() {
Long extensionId = 99L;
ReflectionTestUtils.setField(cmd, "extensionId", extensionId);
assertEquals(extensionId, cmd.getApiResourceId());
}
@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

@ -83,4 +83,52 @@ public class ExtensionResourceMapDaoImplTest {
when(dao.listResourceIdsByExtensionIdAndType(1L, ExtensionResourceMap.ResourceType.Cluster)).thenReturn(expectedIds);
assertEquals(expectedIds, dao.listResourceIdsByExtensionIdAndType(1L, ExtensionResourceMap.ResourceType.Cluster));
}
// -----------------------------------------------------------------------
// Tests for new methods: findResourceByExtensionIdAndResourceIdAndType, listResourceIdsByType
// -----------------------------------------------------------------------
@Test
public void findResourceByExtensionIdAndResourceIdAndTypeReturnsNullWhenNoMatch() {
when(dao.findResourceByExtensionIdAndResourceIdAndType(999L, 77L, ExtensionResourceMap.ResourceType.PhysicalNetwork)).thenReturn(null);
assertNull(dao.findResourceByExtensionIdAndResourceIdAndType(999L, 77L, ExtensionResourceMap.ResourceType.PhysicalNetwork));
}
@Test
public void findResourceByExtensionIdAndResourceIdAndTypeReturnsMatchingEntry() {
ExtensionResourceMapVO map1 = new ExtensionResourceMapVO();
map1.setExtensionId(42L);
map1.setResourceId(7L);
map1.setResourceType(ExtensionResourceMap.ResourceType.PhysicalNetwork);
ExtensionResourceMapVO expected = map1;
when(dao.findResourceByExtensionIdAndResourceIdAndType(42L, 7L, ExtensionResourceMap.ResourceType.PhysicalNetwork)).thenReturn(expected);
ExtensionResourceMapVO result = dao.findResourceByExtensionIdAndResourceIdAndType(42L, 7L, ExtensionResourceMap.ResourceType.PhysicalNetwork);
assertEquals(expected, result);
}
@Test
public void findResourceByExtensionIdAndResourceIdAndTypeDifferentiatesResourceTypes() {
ExtensionResourceMapVO clusterMap = new ExtensionResourceMapVO();
clusterMap.setResourceType(ExtensionResourceMap.ResourceType.Cluster);
when(dao.findResourceByExtensionIdAndResourceIdAndType(10L, 55L, ExtensionResourceMap.ResourceType.Cluster)).thenReturn(clusterMap);
when(dao.findResourceByExtensionIdAndResourceIdAndType(10L, 55L, ExtensionResourceMap.ResourceType.PhysicalNetwork)).thenReturn(null);
assertEquals(clusterMap, dao.findResourceByExtensionIdAndResourceIdAndType(10L, 55L, ExtensionResourceMap.ResourceType.Cluster));
assertNull(dao.findResourceByExtensionIdAndResourceIdAndType(10L, 55L, ExtensionResourceMap.ResourceType.PhysicalNetwork));
}
@Test
public void listResourceIdsByTypeReturnsEmptyListWhenNoMatch() {
when(dao.listResourceIdsByType(ExtensionResourceMap.ResourceType.PhysicalNetwork)).thenReturn(List.of());
assertTrue(dao.listResourceIdsByType(ExtensionResourceMap.ResourceType.PhysicalNetwork).isEmpty());
}
@Test
public void listResourceIdsByTypeReturnsMatchingIds() {
List<Long> expectedIds = List.of(5L, 10L, 15L);
when(dao.listResourceIdsByType(ExtensionResourceMap.ResourceType.PhysicalNetwork)).thenReturn(expectedIds);
List<Long> result = dao.listResourceIdsByType(ExtensionResourceMap.ResourceType.PhysicalNetwork);
assertEquals(3, result.size());
assertEquals(expectedIds, result);
}
}

View File

@ -1573,6 +1573,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) {
@ -3316,9 +3318,11 @@ public class ApiResponseHelper implements ResponseGenerator, ResourceIdSupport {
}
response.setServices(services);
Provider serviceProvider = Provider.getProvider(result.getProviderName());
boolean canEnableIndividualServices = ApiDBUtils.canElementEnableIndividualServices(serviceProvider);
response.setCanEnableIndividualServices(canEnableIndividualServices);
Provider serviceProvider = networkModel.resolveProvider(result.getProviderName());
if (serviceProvider != null) {
boolean canEnableIndividualServices = ApiDBUtils.canElementEnableIndividualServices(serviceProvider);
response.setCanEnableIndividualServices(canEnableIndividualServices);
}
response.setObjectName("networkserviceprovider");
return response;

View File

@ -7308,7 +7308,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);
}
@ -7994,7 +7994,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,9 @@ 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.Extension;
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 +237,11 @@ public class NetworkModelImpl extends ManagerBase implements NetworkModel, Confi
private NetworkService _networkService;
@Inject
TungstenGuestNetworkIpAddressDao tungstenGuestNetworkIpAddressDao;
@Inject
ExtensionHelper extensionHelper;
@Inject
NetworkExtensionElement networkExtensionElement;
private final HashMap<String, NetworkOfferingVO> _systemNetworks = new HashMap<String, NetworkOfferingVO>(5);
@ -262,15 +270,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 +315,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 +485,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 +534,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 +776,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 +816,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 +855,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 +1086,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 +1267,11 @@ public class NetworkModelImpl extends ManagerBase implements NetworkModel, Confi
if (providers == null) {
providers = new HashSet<Provider>();
}
providers.add(Provider.getProvider(instance.getProvider()));
final Provider provider = resolveProvider(instance.getProvider());
if (provider != null) {
providers.add(provider);
}
serviceProviderMap.put(Service.getService(service), providers);
}
@ -1201,9 +1311,98 @@ public class NetworkModelImpl extends ManagerBase implements NetworkModel, Confi
}
}
// Also include extension-backed NetworkExtension providers.
// resolveProvider() creates a transient Provider (not added to the static list)
// for extension names that are not in the built-in registry.
try {
List<PhysicalNetworkServiceProviderVO> nsps = _pNSPDao.listAll();
List<Extension> extensions = extensionHelper.listExtensionsByType(Extension.Type.NetworkOrchestrator);
if (CollectionUtils.isNotEmpty(nsps)) {
Set<String> extensionProviderNames = new HashSet<>();
if (extensions != null) {
for (Extension extension : extensions) {
if (extension == null || StringUtils.isBlank(extension.getName())) {
continue;
}
extensionProviderNames.add(extension.getName());
}
}
if (!extensionProviderNames.isEmpty()) {
// Avoid duplicate provider names across multiple physical networks.
Set<String> addedExtProviders = new HashSet<>();
for (PhysicalNetworkServiceProviderVO nsp : nsps) {
String provName = nsp.getProviderName();
if (StringUtils.isBlank(provName)) {
continue;
}
if (addedExtProviders.contains(provName) || !extensionProviderNames.contains(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;
}
// Check if the network extension provider supports the service
NetworkElement element = getElementImplementingProvider(provName);
if (element == null) {
continue;
}
Map<Service, Map<Capability, String>> elementCapabilities = element.getCapabilities();
if (elementCapabilities == null || !elementCapabilities.containsKey(service)) {
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#listAll})
* 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 +1739,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 +1785,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 +1860,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 +1905,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 +2189,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 +2491,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

@ -2975,7 +2975,7 @@ public class NetworkServiceImpl extends ManagerBase implements NetworkService, C
throw new InvalidParameterValueException("Network is not in the right state to be restarted. Correct states are: " + Network.State.Implemented + ", " + Network.State.Setup);
}
if (network.getBroadcastDomainType() == BroadcastDomainType.Lswitch) {
if (network.getBroadcastDomainType() == BroadcastDomainType.Lswitch && !_networkMgr.isIsolationMethodNetworkExtension(network.getNetworkOfferingId())) {
/**
* Unable to restart these networks now.
* TODO Restarting a SDN based network requires updating the nics and the configuration
@ -3087,8 +3087,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;
}
}
@ -4449,6 +4449,7 @@ public class NetworkServiceImpl extends ManagerBase implements NetworkService, C
addOrRemoveVnets(listOfRanges, network);
}
_physicalNetworkDao.update(id, network);
return network;
}
@ -5121,7 +5122,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 +5159,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

@ -1482,7 +1482,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");
}
@ -1551,7 +1551,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,13 +622,26 @@ 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) {
supportedProtocols = caps.get(Capability.SupportedEgressProtocols).toLowerCase();
// 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 = supportedProtocolsStr.toLowerCase();
} else {
supportedProtocols = caps.get(Capability.SupportedProtocols).toLowerCase();
String supportedProtocolsStr = caps.get(Capability.SupportedProtocols);
if (supportedProtocolsStr == null) {
throw new CloudRuntimeException("Supported protocols capability is not defined for " + purpose + " service");
}
supportedProtocols = supportedProtocolsStr.toLowerCase();
}
if (!supportedProtocols.contains(proto.toLowerCase())) {
@ -700,18 +718,38 @@ 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);
if (element instanceof FirewallServiceProvider) {
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);
if (element instanceof PortForwardingServiceProvider) {
handled = ((PortForwardingServiceProvider) element).applyPFRules(network, (List<PortForwardingRule>) rules);
}
}
}
break;
/* case NetworkACL:
for (NetworkACLServiceProvider element: _networkAclElements) {
@ -726,7 +764,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

@ -0,0 +1,162 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package com.cloud.network.guru;
import java.util.List;
import javax.inject.Inject;
import com.cloud.dc.DataCenter;
import com.cloud.deploy.DeployDestination;
import com.cloud.deploy.DeploymentPlan;
import com.cloud.exception.InsufficientAddressCapacityException;
import com.cloud.exception.InsufficientVirtualNetworkCapacityException;
import com.cloud.network.Network;
import com.cloud.network.Network.GuestType;
import com.cloud.network.Network.State;
import com.cloud.network.PhysicalNetwork;
import com.cloud.network.dao.NetworkVO;
import com.cloud.offering.NetworkOffering;
import com.cloud.user.Account;
import com.cloud.vm.NicProfile;
import com.cloud.vm.ReservationContext;
import com.cloud.vm.VirtualMachineProfile;
import org.apache.cloudstack.extension.ExtensionHelper;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
/**
* Guest network guru for extension-backed providers on physical networks whose
* isolation method is set to "Extension".
*
* <p>For <b>isolated</b> networks the allocation path is identical to
* {@link GuestNetworkGuru}: a virtual CIDR is managed internally and IPs are
* taken from that pool.</p>
*
* <p>For <b>shared</b> networks (upstream router as gateway) the
* allocation is delegated entirely to {@link DirectNetworkGuru}</p>
*/
public class NetworkExtensionGuestNetworkGuru extends GuestNetworkGuru {
@Inject
protected ExtensionHelper extensionHelper;
@Autowired
@Qualifier("DirectNetworkGuru")
protected DirectNetworkGuru directNetworkGuru;
@Override
protected boolean canHandle(NetworkOffering offering, DataCenter.NetworkType networkType, PhysicalNetwork physicalNetwork) {
List<String> providers = networkOfferingServiceMapDao.getDistinctProviders(offering.getId());
if (CollectionUtils.isEmpty(providers)) {
return false;
}
return providers.stream().anyMatch(providerName ->
extensionHelper.isNetworkExtensionProvider(providerName)
&& physicalNetwork != null
&& extensionHelper.usesNetworkExtensionIsolation(providerName));
}
/**
* Designs the network and ensures the state is {@code Allocated}, never {@code Setup}.
*
* <p>{@link GuestNetworkGuru#design} sets the state to {@code Setup} when
* {@code offering.isSpecifyVlan() && !offering.isPersistent()}, which means the
* parent considers the physical network already in place and skips
* {@code element.implement()} later.
* For NetworkExtension-based networks Resetting the state to
* {@code Allocated} here guarantees {@code implement-network} is always called.</p>
*/
@Override
public Network design(final NetworkOffering offering, final DeploymentPlan plan,
final Network userSpecified, final String name,
final Long vpcId, final Account owner) {
final Network network = super.design(offering, plan, userSpecified, name, vpcId, owner);
if (network == null) {
return null;
}
if (network instanceof NetworkVO && network.getState() == State.Setup) {
NetworkVO networkVO = (NetworkVO) network;
networkVO.setState(State.Allocated);
return networkVO;
}
return network;
}
@Override
public Network implement(final Network network, final NetworkOffering offering, final DeployDestination dest, final ReservationContext context)
throws InsufficientVirtualNetworkCapacityException {
assert network.getState() == Network.State.Implementing : "Why are we implementing " + network;
final long dcId = dest.getDataCenter().getId();
// physical network id can be null in Guest Network in Basic zone
Long physicalNetworkId = network.getPhysicalNetworkId();
if (physicalNetworkId == null) {
physicalNetworkId = _networkModel.findPhysicalNetworkId(dcId, offering.getTags(), offering.getTrafficType());
}
// The broadcast_domain_type and broadcast_uri are set by the extension script output
final NetworkVO implemented = getNetworkVO(network, offering, physicalNetworkId);
return implemented;
}
private static NetworkVO getNetworkVO(Network network, NetworkOffering offering, Long physicalNetworkId) {
final NetworkVO implemented =
new NetworkVO(network.getTrafficType(), network.getMode(), network.getBroadcastDomainType(), network.getNetworkOfferingId(), Network.State.Allocated,
network.getDataCenterId(), physicalNetworkId, offering.isRedundantRouter());
if (network.getGateway() != null) {
implemented.setGateway(network.getGateway());
}
if (network.getCidr() != null) {
implemented.setCidr(network.getCidr());
}
if (network.getBroadcastUri() != null) {
implemented.setBroadcastUri(network.getBroadcastUri());
}
return implemented;
}
/**
* Allocates a NIC for the given network.
*
* <p>For <b>shared</b> networks the call is forwarded to
* {@link DirectNetworkGuru#allocate}, which handles the
* {@code user_ip_address} lookup, transaction wrapping, and MAC assignment
* identically to how direct/shared networks work elsewhere in CloudStack.</p>
*
* <p>For <b>isolated</b> networks the standard
* {@link GuestNetworkGuru#allocate} path is used unchanged.</p>
*/
@Override
public NicProfile allocate(final Network network, final NicProfile nic, final VirtualMachineProfile vm)
throws InsufficientVirtualNetworkCapacityException, InsufficientAddressCapacityException {
if (network.getGuestType() == GuestType.Shared) {
// Delegate entirely to DirectNetworkGuru
return directNetworkGuru.allocate(network, nic, vm);
}
// Isolated network: use the standard GuestNetworkGuru CIDR pool allocation.
return super.allocate(network, nic, vm);
}
}

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;
@ -2055,6 +2056,16 @@ 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);
if (element instanceof LoadBalancingServiceProvider) {
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,26 @@ 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);
if (element instanceof NetworkACLServiceProvider) {
logger.debug("Applying NetworkACL for network: {} with Network ACL service provider: {}", network, 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,12 @@ public class VpcManagerImpl extends ManagerBase implements VpcManager, VpcProvis
if (provider == null) {
// Default to VPCVirtualRouter
provider = Provider.VPCVirtualRouter.getName();
} else {
final Provider resolvedProvider = _ntwkModel.resolveProvider(provider);
if (resolvedProvider == null) {
throw new InvalidParameterValueException("Invalid provider " + provider + " configured for service " + service);
}
provider = resolvedProvider.getName();
}
if (!_ntwkModel.isProviderEnabledInZone(zoneId, provider)) {
@ -2035,9 +2051,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 +2530,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 +2638,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 +3972,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

@ -33,6 +33,9 @@
<bean id="ExternalGuestNetworkGuru" class="com.cloud.network.guru.ExternalGuestNetworkGuru">
<property name="name" value="ExternalGuestNetworkGuru" />
</bean>
<bean id="NetworkExtensionGuestNetworkGuru" class="com.cloud.network.guru.NetworkExtensionGuestNetworkGuru">
<property name="name" value="NetworkExtensionGuestNetworkGuru" />
</bean>
<bean id="PublicNetworkGuru" class="com.cloud.network.guru.PublicNetworkGuru">
<property name="name" value="PublicNetworkGuru" />
</bean>

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

@ -18,6 +18,9 @@ package com.cloud.network;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
@ -54,6 +57,9 @@ import com.cloud.network.dao.NetworkDao;
import com.cloud.network.dao.NetworkServiceMapDao;
import com.cloud.network.dao.NetworkServiceMapVO;
import com.cloud.network.dao.NetworkVO;
import com.cloud.network.dao.PhysicalNetworkDao;
import com.cloud.network.dao.PhysicalNetworkServiceProviderDao;
import com.cloud.network.dao.PhysicalNetworkServiceProviderVO;
import com.cloud.network.element.NetworkElement;
import com.cloud.network.element.VpcVirtualRouterElement;
import com.cloud.network.vpc.VpcVO;
@ -66,6 +72,9 @@ 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.Extension;
import org.apache.cloudstack.extension.ExtensionHelper;
import org.apache.cloudstack.framework.extensions.network.NetworkExtensionElement;
@RunWith(MockitoJUnitRunner.class)
public class NetworkModelImplTest {
@ -80,6 +89,17 @@ public class NetworkModelImplTest {
private NetworkDao _networksDao;
@Inject
private NetworkOfferingServiceMapDao networkOfferingServiceMapDao;
@Mock
private ExtensionHelper extensionHelper;
@Mock
private NetworkExtensionElement networkExtensionElement;
@Mock
private PhysicalNetworkDao physicalNetworkDao;
@Mock
private PhysicalNetworkServiceProviderDao physicalNetworkServiceProviderDao;
@Spy
@InjectMocks
@ -96,6 +116,11 @@ public class NetworkModelImplTest {
networkModel._networkOfferingDao = networkOfferingDao;
networkModel._ntwkSrvcDao = networkServiceMapDao;
networkModel._ntwkOfferingSrvcDao = networkOfferingServiceMapDao;
networkModel.extensionHelper = extensionHelper;
networkModel.networkExtensionElement = networkExtensionElement;
networkModel._physicalNetworkDao = physicalNetworkDao;
networkModel._pNSPDao = physicalNetworkServiceProviderDao;
Mockito.lenient().when(extensionHelper.isNetworkExtensionProvider(Mockito.anyString())).thenReturn(false);
}
private void prepareMocks(boolean isIp6, Network network, DataCenter zone, VpcVO vpc,
@ -242,8 +267,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));
@ -273,4 +298,158 @@ public class NetworkModelImplTest {
assertNotNull(result);
}
// -----------------------------------------------------------------------
// Tests for getElementImplementingProvider with extension provider
// -----------------------------------------------------------------------
@Test
public void getElementImplementingProviderReturnsExtensionElementForExtensionProvider() {
String providerName = "my-ext-provider";
// Provider is not in the static map, so element would be null
ReflectionTestUtils.setField(networkModel, "networkElements", new ArrayList<>());
when(extensionHelper.isNetworkExtensionProvider(providerName)).thenReturn(true);
NetworkExtensionElement mockElement = mock(NetworkExtensionElement.class);
when(networkExtensionElement.withProviderName(providerName)).thenReturn(mockElement);
NetworkElement result = networkModel.getElementImplementingProvider(providerName);
// When the element is a NetworkExtensionElement (which is a NetworkElement), result should not be null
assertNotNull(result);
}
@Test
public void getElementImplementingProviderReturnsNullForUnknownNonExtensionProvider() {
String providerName = "unknown-provider";
ReflectionTestUtils.setField(networkModel, "networkElements", new ArrayList<>());
when(extensionHelper.isNetworkExtensionProvider(providerName)).thenReturn(false);
NetworkElement result = networkModel.getElementImplementingProvider(providerName);
assertNull(result);
}
// -----------------------------------------------------------------------
// Tests for resolveProvider
// -----------------------------------------------------------------------
@Test
public void resolveProviderReturnsKnownProvider() {
Network.Provider result = networkModel.resolveProvider(Network.Provider.VirtualRouter.getName());
assertNotNull(result);
assertEquals(Network.Provider.VirtualRouter, result);
}
@Test
public void resolveProviderReturnsTransientProviderForExtensionProvider() {
String extensionName = "my-ext-network-provider";
when(extensionHelper.isNetworkExtensionProvider(extensionName)).thenReturn(true);
Network.Provider result = networkModel.resolveProvider(extensionName);
assertNotNull(result);
assertEquals(extensionName, result.getName());
}
@Test
public void resolveProviderReturnsNullForUnknownNonExtensionProvider() {
String providerName = "totally-unknown";
when(extensionHelper.isNetworkExtensionProvider(providerName)).thenReturn(false);
Network.Provider result = networkModel.resolveProvider(providerName);
assertNull(result);
}
// -----------------------------------------------------------------------
// Tests for canElementEnableIndividualServicesByName
// -----------------------------------------------------------------------
@Test
public void canElementEnableIndividualServicesByNameReturnsFalseForNullProvider() {
assertFalse(networkModel.canElementEnableIndividualServicesByName(null));
}
@Test
public void canElementEnableIndividualServicesByNameReturnsFalseForUnknownProvider() {
when(extensionHelper.isNetworkExtensionProvider("unknown")).thenReturn(false);
assertFalse(networkModel.canElementEnableIndividualServicesByName("unknown"));
}
// -----------------------------------------------------------------------
// Tests for getExternalProviderCapabilities
// -----------------------------------------------------------------------
@Test
public void getExternalProviderCapabilitiesCallsExtensionHelper() {
Map<Network.Service, Map<Network.Capability, String>> caps = new HashMap<>();
when(extensionHelper.getNetworkCapabilitiesForProvider(10L, "my-ext")).thenReturn(caps);
Map<Network.Service, Map<Network.Capability, String>> result =
networkModel.getExternalProviderCapabilities(10L, "my-ext");
assertEquals(caps, result);
}
// -----------------------------------------------------------------------
// Tests for isServiceProvidedByNsp (via listSupportedNetworkServiceProviders)
// -----------------------------------------------------------------------
@Test
public void listSupportedNetworkServiceProvidersIncludesExtensionBackedProvidersWhenServiceIsSupported() {
PhysicalNetworkServiceProviderVO nsp = mock(PhysicalNetworkServiceProviderVO.class);
when(nsp.getProviderName()).thenReturn("my-ext");
when(nsp.isFirewallServiceProvided()).thenReturn(true);
when(physicalNetworkServiceProviderDao.listAll()).thenReturn(List.of(nsp));
Extension extension = mock(Extension.class);
when(extension.getName()).thenReturn("my-ext");
when(extensionHelper.listExtensionsByType(Extension.Type.NetworkOrchestrator)).thenReturn(List.of(extension));
when(extensionHelper.isNetworkExtensionProvider("my-ext")).thenReturn(true);
// networkElements is empty so no standard providers found
ReflectionTestUtils.setField(networkModel, "networkElements", new ArrayList<>());
NetworkExtensionElement mockElement = mock(NetworkExtensionElement.class);
Map<Network.Service, Map<Network.Capability, String>> capabilities = new HashMap<>();
capabilities.put(Network.Service.Firewall, new HashMap<>());
when(networkExtensionElement.withProviderName("my-ext")).thenReturn(mockElement);
when(mockElement.getCapabilities()).thenReturn(capabilities);
List<? extends Network.Provider> result =
networkModel.listSupportedNetworkServiceProviders(Network.Service.Firewall.getName());
boolean found = result.stream().anyMatch(p -> "my-ext".equalsIgnoreCase(p.getName()));
assertTrue("Extension-backed provider should be included", found);
Mockito.verify(physicalNetworkServiceProviderDao, Mockito.times(1)).listAll();
Mockito.verify(physicalNetworkServiceProviderDao, Mockito.never()).listBy(Mockito.anyLong());
}
@Test
public void listSupportedNetworkServiceProvidersExcludesExtensionBackedProvidersWhenServiceIsNotSupported() {
PhysicalNetworkServiceProviderVO nsp = mock(PhysicalNetworkServiceProviderVO.class);
when(nsp.getProviderName()).thenReturn("my-ext");
when(nsp.isFirewallServiceProvided()).thenReturn(true);
when(physicalNetworkServiceProviderDao.listAll()).thenReturn(List.of(nsp));
Extension extension = mock(Extension.class);
when(extension.getName()).thenReturn("my-ext");
when(extensionHelper.listExtensionsByType(Extension.Type.NetworkOrchestrator)).thenReturn(List.of(extension));
when(extensionHelper.isNetworkExtensionProvider("my-ext")).thenReturn(true);
// networkElements is empty so no standard providers found
ReflectionTestUtils.setField(networkModel, "networkElements", new ArrayList<>());
NetworkExtensionElement mockElement = mock(NetworkExtensionElement.class);
Map<Network.Service, Map<Network.Capability, String>> capabilities = new HashMap<>();
when(networkExtensionElement.withProviderName("my-ext")).thenReturn(mockElement);
when(mockElement.getCapabilities()).thenReturn(capabilities);
List<? extends Network.Provider> result =
networkModel.listSupportedNetworkServiceProviders(Network.Service.Firewall.getName());
boolean found = result.stream().anyMatch(p -> "my-ext".equalsIgnoreCase(p.getName()));
assertFalse("Extension-backed provider should NOT be included", found);
Mockito.verify(physicalNetworkServiceProviderDao, Mockito.times(1)).listAll();
Mockito.verify(physicalNetworkServiceProviderDao, Mockito.never()).listBy(Mockito.anyLong());
}
}

View File

@ -427,6 +427,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

@ -0,0 +1,178 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package com.cloud.network.firewall;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.Collections;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.test.util.ReflectionTestUtils;
import com.cloud.exception.ResourceUnavailableException;
import com.cloud.network.Network;
import com.cloud.network.NetworkModel;
import com.cloud.network.dao.NetworkServiceMapDao;
import com.cloud.network.element.FirewallServiceProvider;
import com.cloud.network.element.NetworkElement;
import com.cloud.network.element.PortForwardingServiceProvider;
import com.cloud.network.rules.FirewallRule;
import com.cloud.network.rules.PortForwardingRule;
@RunWith(MockitoJUnitRunner.Silent.class)
public class FirewallManagerImplTest {
interface MockFwElement extends NetworkElement, FirewallServiceProvider {}
interface MockPfElement extends NetworkElement, PortForwardingServiceProvider {}
@Spy
@InjectMocks
private FirewallManagerImpl firewallManager;
@Mock
private NetworkModel _networkModel;
@Mock
private NetworkServiceMapDao networkServiceMapDao;
@Before
public void setUp() {
ReflectionTestUtils.setField(firewallManager, "_firewallElements", Collections.emptyList());
ReflectionTestUtils.setField(firewallManager, "_pfElements", Collections.emptyList());
ReflectionTestUtils.setField(firewallManager, "_staticNatElements", Collections.emptyList());
ReflectionTestUtils.setField(firewallManager, "_networkAclElements", Collections.emptyList());
}
// -----------------------------------------------------------------------
// Tests for applyRules with extension-backed Firewall provider
// -----------------------------------------------------------------------
@Test
public void applyRulesFirewallHandledByExtensionProvider() throws ResourceUnavailableException {
Network network = mock(Network.class);
when(network.getId()).thenReturn(1L);
FirewallRule rule = mock(FirewallRule.class);
List<? extends FirewallRule> rules = List.of(rule);
// No standard firewall elements handle it
String extProviderName = "my-ext-fw-provider";
when(networkServiceMapDao.getProviderForServiceInNetwork(1L, Network.Service.Firewall))
.thenReturn(extProviderName);
// The element implementing the provider is both NetworkElement and FirewallServiceProvider
MockFwElement element = mock(MockFwElement.class);
when(element.applyFWRules(eq(network), any())).thenReturn(true);
when(_networkModel.getElementImplementingProvider(extProviderName)).thenReturn(element);
boolean result = firewallManager.applyRules(network, FirewallRule.Purpose.Firewall, rules);
assertTrue(result);
verify(element).applyFWRules(eq(network), any());
}
@Test
public void applyRulesFirewallReturnsFalseWhenNoExtensionProviderFound() throws ResourceUnavailableException {
Network network = mock(Network.class);
when(network.getId()).thenReturn(2L);
FirewallRule rule = mock(FirewallRule.class);
List<? extends FirewallRule> rules = List.of(rule);
// No standard provider and no extension provider found
when(networkServiceMapDao.getProviderForServiceInNetwork(2L, Network.Service.Firewall))
.thenReturn(null);
boolean result = firewallManager.applyRules(network, FirewallRule.Purpose.Firewall, rules);
assertFalse(result);
verify(_networkModel, never()).getElementImplementingProvider(any());
}
// -----------------------------------------------------------------------
// Tests for applyRules with extension-backed PortForwarding provider
// -----------------------------------------------------------------------
@Test
public void applyRulesPortForwardingHandledByExtensionProvider() throws ResourceUnavailableException {
Network network = mock(Network.class);
when(network.getId()).thenReturn(3L);
PortForwardingRule rule = mock(PortForwardingRule.class);
@SuppressWarnings("unchecked")
List<PortForwardingRule> rules = List.of(rule);
String extProviderName = "my-ext-pf-provider";
when(networkServiceMapDao.getProviderForServiceInNetwork(3L, Network.Service.PortForwarding))
.thenReturn(extProviderName);
MockPfElement element = mock(MockPfElement.class);
when(element.applyPFRules(eq(network), any())).thenReturn(true);
when(_networkModel.getElementImplementingProvider(extProviderName)).thenReturn(element);
boolean result = firewallManager.applyRules(network, FirewallRule.Purpose.PortForwarding, rules);
assertTrue(result);
verify(element).applyPFRules(eq(network), any());
}
@Test
public void applyRulesPortForwardingReturnsFalseWhenNoExtensionProviderFound() throws ResourceUnavailableException {
Network network = mock(Network.class);
when(network.getId()).thenReturn(4L);
PortForwardingRule rule = mock(PortForwardingRule.class);
List<? extends FirewallRule> rules = List.of(rule);
when(networkServiceMapDao.getProviderForServiceInNetwork(4L, Network.Service.PortForwarding))
.thenReturn(null);
boolean result = firewallManager.applyRules(network, FirewallRule.Purpose.PortForwarding, rules);
assertFalse(result);
}
// -----------------------------------------------------------------------
// Tests for StaticNat (handled by Firewall elements)
// -----------------------------------------------------------------------
@Test
public void applyRulesStaticNatReturnsFalseWhenNoProviderFound() throws ResourceUnavailableException {
Network network = mock(Network.class);
when(network.getId()).thenReturn(5L);
FirewallRule rule = mock(FirewallRule.class);
List<? extends FirewallRule> rules = List.of(rule);
when(networkServiceMapDao.getProviderForServiceInNetwork(5L, Network.Service.Firewall))
.thenReturn(null);
boolean result = firewallManager.applyRules(network, FirewallRule.Purpose.StaticNat, rules);
assertFalse(result);
}
}

View File

@ -0,0 +1,110 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package com.cloud.network.guru;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.when;
import java.util.List;
import org.apache.cloudstack.extension.ExtensionHelper;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import com.cloud.dc.DataCenter.NetworkType;
import com.cloud.network.Network;
import com.cloud.network.Network.GuestType;
import com.cloud.network.Networks.TrafficType;
import com.cloud.network.dao.PhysicalNetworkVO;
import com.cloud.offering.NetworkOffering;
import com.cloud.offerings.dao.NetworkOfferingServiceMapDao;
public class NetworkExtensionGuestNetworkGuruTest {
private final NetworkExtensionGuestNetworkGuru guru = new NetworkExtensionGuestNetworkGuru();
@Mock
private NetworkOffering offering;
@Mock
private PhysicalNetworkVO physicalNetwork;
@Mock
private NetworkOfferingServiceMapDao networkOfferingServiceMapDao;
@Mock
private ExtensionHelper extensionHelper;
private AutoCloseable closeable;
@Before
public void setup() {
closeable = MockitoAnnotations.openMocks(this);
guru.networkOfferingServiceMapDao = networkOfferingServiceMapDao;
guru.extensionHelper = extensionHelper;
when(offering.getId()).thenReturn(42L);
when(offering.getTrafficType()).thenReturn(TrafficType.Guest);
when(offering.getGuestType()).thenReturn(GuestType.Isolated);
when(offering.isSystemOnly()).thenReturn(false);
when(physicalNetwork.getId()).thenReturn(7L);
}
@After
public void tearDown() throws Exception {
closeable.close();
}
@Test
public void canHandleWhenOfferingUsesStaticNetworkExtensionProvider() {
String provider = Network.Provider.NetworkExtension.getName();
when(networkOfferingServiceMapDao.getDistinctProviders(offering.getId())).thenReturn(List.of(provider));
when(extensionHelper.isNetworkExtensionProvider(provider)).thenReturn(true);
when(extensionHelper.usesNetworkExtensionIsolation(provider)).thenReturn(true);
assertTrue(guru.canHandle(offering, NetworkType.Advanced, physicalNetwork));
}
@Test
public void canHandleWhenOfferingUsesDynamicExtensionProvider() {
when(networkOfferingServiceMapDao.getDistinctProviders(offering.getId())).thenReturn(List.of("my-ext"));
when(extensionHelper.isNetworkExtensionProvider("my-ext")).thenReturn(true);
when(extensionHelper.usesNetworkExtensionIsolation("my-ext")).thenReturn(true);
assertTrue(guru.canHandle(offering, NetworkType.Advanced, physicalNetwork));
}
@Test
public void cannotHandleWhenIsolationMethodDetailIsNotExtension() {
when(networkOfferingServiceMapDao.getDistinctProviders(offering.getId())).thenReturn(List.of("my-ext"));
when(extensionHelper.isNetworkExtensionProvider("my-ext")).thenReturn(true);
when(extensionHelper.usesNetworkExtensionIsolation("my-ext")).thenReturn(false);
assertFalse(guru.canHandle(offering, NetworkType.Advanced, physicalNetwork));
}
@Test
public void cannotHandleWhenNoExtensionProvidersArePresent() {
when(networkOfferingServiceMapDao.getDistinctProviders(offering.getId()))
.thenReturn(List.of(Network.Provider.VirtualRouter.getName()));
when(extensionHelper.isNetworkExtensionProvider(Network.Provider.VirtualRouter.getName())).thenReturn(false);
assertFalse(guru.canHandle(offering, NetworkType.Advanced, physicalNetwork));
}
}

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

@ -43,6 +43,7 @@ import com.cloud.network.dao.IPAddressDao;
import com.cloud.network.dao.IPAddressVO;
import com.cloud.network.dao.NetworkDao;
import com.cloud.network.element.NetworkElement;
import com.cloud.network.element.VpcProvider;
import com.cloud.network.router.CommandSetupHelper;
import com.cloud.network.router.NetworkHelper;
import com.cloud.network.router.VirtualRouter;
@ -72,6 +73,8 @@ import org.apache.cloudstack.api.command.admin.vpc.CreateVPCOfferingCmd;
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.network.Ipv4GuestSubnetNetworkMap;
import org.apache.cloudstack.network.RoutedIpv4Manager;
@ -90,6 +93,7 @@ import org.springframework.test.util.ReflectionTestUtils;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@ -99,6 +103,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 +252,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() {
@ -601,4 +613,49 @@ public class VpcManagerImplTest {
Assert.assertTrue(manager.isNetworkOnVpcEnabledConserveMode(network));
}
// -----------------------------------------------------------------------
// Tests for getVpcElements with extension-backed NetworkOrchestrator
// -----------------------------------------------------------------------
@Test
public void getVpcElementsIncludesExtensionBackedVpcProvider() {
manager.setVpcElements(null);
Mockito.when(networkModel.getElementImplementingProvider(Provider.VPCVirtualRouter.getName())).thenReturn(null);
Mockito.when(networkModel.getElementImplementingProvider(Provider.JuniperContrailVpcRouter.getName())).thenReturn(null);
Extension ext = mock(Extension.class);
Mockito.when(ext.getName()).thenReturn("my-vpc-ext");
ExtensionHelper extHelper = mock(ExtensionHelper.class);
Mockito.when(extHelper.listExtensionsByType(Extension.Type.NetworkOrchestrator))
.thenReturn(List.of(ext));
manager.extensionHelper = extHelper;
// The element for the extension also implements VpcProvider
VpcProvider vpcProviderElement = mock(VpcProvider.class);
Mockito.when(networkModel.getElementImplementingProvider("my-vpc-ext")).thenReturn((NetworkElement) vpcProviderElement);
List<VpcProvider> result = manager.getVpcElements();
Assert.assertNotNull(result);
Assert.assertTrue(result.contains(vpcProviderElement));
}
@Test
public void getVpcElementsReturnsEmptyListWhenNoStaticNorExtensionProviders() {
manager.setVpcElements(null);
Mockito.when(networkModel.getElementImplementingProvider(Provider.VPCVirtualRouter.getName())).thenReturn(null);
Mockito.when(networkModel.getElementImplementingProvider(Provider.JuniperContrailVpcRouter.getName())).thenReturn(null);
ExtensionHelper extHelper = mock(ExtensionHelper.class);
Mockito.when(extHelper.listExtensionsByType(Extension.Type.NetworkOrchestrator))
.thenReturn(Collections.emptyList());
manager.extensionHelper = extHelper;
List<VpcProvider> result = manager.getVpcElements();
Assert.assertNotNull(result);
Assert.assertTrue(result.isEmpty());
}
}

View File

@ -585,6 +585,11 @@ public class MockNetworkManagerImpl extends ManagerBase implements NetworkOrches
return null;
}
@Override
public boolean isIsolationMethodNetworkExtension(Long networkOfferingId) {
return false;
}
/* (non-Javadoc)
* @see com.cloud.network.NetworkManager#allocate(com.cloud.vm.VirtualMachineProfile, java.util.List)
*/

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[])
*/

View File

@ -73,6 +73,11 @@ public class MockVpcOfferingServiceMapDaoImpl extends GenericDaoBase<VpcOffering
return List.of();
}
@Override
public List<Long> listOfferingIdsByServiceAndProvider(Service service, String provider) {
return List.of();
}
@Override
public VpcOfferingServiceMapVO persist(VpcOfferingServiceMapVO vo) {
return vo;

File diff suppressed because it is too large Load Diff

View File

@ -46,7 +46,7 @@ setup(name="Marvin",
"marvin.sandbox.basic"],
license="LICENSE.txt",
install_requires=[
"mysql-connector-python <= 8.4.0",
"mysql-connector-python >= 8.4.0",
"requests >= 2.2.1",
"paramiko >= 1.13.0",
"nose >= 1.3.3",

View File

@ -245,6 +245,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",
@ -1142,6 +1143,7 @@
"label.external.details.tooltip": "Details that will be passed to the external provisioner while deploying an instance",
"label.externalprovisioner": "External provisioner",
"label.external.link": "External link",
"label.external.network.provider": "External Network Provider",
"label.externalid": "External Id",
"label.externalloadbalanceripaddress": "External load balancer IP address.",
"label.extra": "Extra arguments",
@ -1801,6 +1803,7 @@
"label.networkofferingdisplaytext": "Network Offering",
"label.networkofferingid": "Network Offering",
"label.networkofferingname": "Network Offering",
"label.networkorchestrator": "Network Orchestrator",
"label.networkrate": "Network rate (Mb/s)",
"label.networkread": "Network read",
"label.networks": "Networks",
@ -4301,6 +4304,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",

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

@ -121,6 +121,14 @@ export default {
name: 'network.permissions',
component: shallowRef(defineAsyncComponent(() => import('@/views/network/NetworkPermissions.vue'))),
show: (record, route, user) => { return 'listNetworkPermissions' in store.getters.apis && record.acltype === 'Account' && !('vpcid' in record) && (['Admin', 'DomainAdmin'].includes(user.roletype) || record.account === user.account) && !record.projectid }
}, {
name: 'custom.actions',
component: shallowRef(defineAsyncComponent(() => import('@/views/extension/RunCustomAction.vue'))),
show: (record) => {
return 'runCustomAction' in store.getters.apis &&
'listCustomActions' in store.getters.apis &&
record.service && record.service.some(s => s.name === 'CustomAction')
}
},
{
name: 'events',
@ -210,6 +218,19 @@ export default {
}
}
},
{
api: 'runCustomAction',
icon: 'thunderbolt-outlined',
label: 'label.run.custom.action',
dataView: true,
show: (record) => {
return 'runCustomAction' in store.getters.apis &&
'listCustomActions' in store.getters.apis &&
record.service && record.service.some(s => s.name === 'CustomAction')
},
popup: true,
component: shallowRef(defineAsyncComponent(() => import('@/views/extension/RunCustomAction.vue')))
},
{
api: 'associateDnsZoneToNetwork',
icon: 'link-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', 'Vpc'],
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', 'Vpc']
this.form.resourcetype = 'Network'
} else if (type === 'Orchestrator') {
this.resourceTypeOptions = ['VirtualMachine']
this.form.resourcetype = 'VirtualMachine'
} else {
this.resourceTypeOptions = ['VirtualMachine', 'Network', 'Vpc']
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,14 @@
{{ text && $toLocaleDate(text) }}
</template>
<template v-if="column.key === 'actions'">
<span style="margin-right: 5px">
<tooltip-button
v-if="'updateRegisteredExtension' in $store.getters.apis"
: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 +67,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 +89,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 +128,9 @@ export default {
title: this.$t('label.actions')
}
],
unregisterLoading: false
unregisterLoading: false,
updateModalVisible: false,
selectedResource: null
}
},
computed: {
@ -112,13 +139,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 +168,7 @@ export default {
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.deleteLoading = false
this.unregisterLoading = false
})
}
}

View File

@ -0,0 +1,268 @@
// 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.
<script>
import { defineComponent, ref, h, provide, inject, watch } from 'vue'
const INDENT = 18 // px per depth level
const C = {
key: '#0451a5',
string: '#a31515',
number: '#098658',
boolean: '#0000ff',
null: '#767676',
punct: '#333333',
arrow: '#888888',
summary: '#888888'
}
const JsonNode = defineComponent({
name: 'JsonNode',
props: {
data: {},
keyName: { type: String, default: null },
depth: { type: Number, default: 0 },
isLast: { type: Boolean, default: true }
},
setup (props) {
// jeExpandMode tracks the last toolbar action so newly-mounted children
// (revealed when their parent expands) pick up the correct initial state.
const expandMode = inject('jeExpandMode', ref(null)) // null | 'expanded' | 'collapsed'
const collapsed = ref(expandMode.value === 'expanded' ? false : props.depth >= 2)
const toggle = () => { collapsed.value = !collapsed.value }
const expandSignal = inject('jeExpand', ref(0))
const collapseSignal = inject('jeCollapse', ref(0))
watch(expandSignal, () => { collapsed.value = false })
watch(collapseSignal, () => { collapsed.value = props.depth >= 2 })
return () => {
const { data, keyName, depth, isLast } = props
const pl = `${depth * INDENT}px`
// Key fragment: "name":·
const keyFrag = keyName !== null
? [
h('span', { style: { color: C.key } }, `"${keyName}"`),
h('span', { style: { color: C.punct } }, ': ')
]
: []
const comma = !isLast ? h('span', { style: { color: C.punct } }, ',') : null
// null
if (data === null) {
return h('div', { style: { paddingLeft: pl } }, [
...keyFrag,
h('span', { style: { color: C.null } }, 'null'),
comma
])
}
const type = Array.isArray(data) ? 'array' : typeof data
// Object / Array
if (type === 'object' || type === 'array') {
const entries = type === 'array'
? data.map((v, i) => [i, v])
: Object.entries(data)
const count = entries.length
const open = type === 'array' ? '[' : '{'
const close = type === 'array' ? ']' : '}'
if (count === 0) {
return h('div', { style: { paddingLeft: pl } }, [
...keyFrag,
h('span', { style: { color: C.punct } }, `${open}${close}`),
comma
])
}
const arrowSt = {
display: 'inline-block',
width: '14px',
color: C.arrow,
cursor: 'pointer',
userSelect: 'none',
fontSize: '9px',
verticalAlign: 'middle'
}
const summary = type === 'array'
? `${count} item${count !== 1 ? 's' : ''}`
: `${count} key${count !== 1 ? 's' : ''}`
if (collapsed.value) {
return h('div', {
style: { paddingLeft: pl, cursor: 'pointer', whiteSpace: 'nowrap' },
onClick: toggle
}, [
h('span', { style: arrowSt }, '▶'),
...keyFrag,
h('span', { style: { color: C.punct } }, `${open} `),
h('span', { style: { color: C.summary, fontStyle: 'italic' } }, summary),
h('span', { style: { color: C.punct } }, ` ${close}`),
comma
])
}
return h('div', {}, [
// Opening line with toggle
h('div', { style: { paddingLeft: pl, cursor: 'pointer' }, onClick: toggle }, [
h('span', { style: arrowSt }, '▼'),
...keyFrag,
h('span', { style: { color: C.punct } }, open)
]),
// Children each child provides its own paddingLeft via (depth+1)*INDENT
...entries.map(([k, v], i) =>
h(JsonNode, {
key: String(k),
data: v,
keyName: type === 'array' ? null : String(k),
depth: depth + 1,
isLast: i === count - 1
})
),
// Closing bracket at same indentation as the opening line
h('div', { style: { paddingLeft: pl } }, [
h('span', { style: { color: C.punct } }, close),
comma
])
])
}
// Primitives
let valueEl
if (type === 'string') {
valueEl = h('span', { style: { color: C.string } }, `"${data}"`)
} else if (type === 'number') {
valueEl = h('span', { style: { color: C.number } }, String(data))
} else {
// boolean
valueEl = h('span', { style: { color: C.boolean } }, String(data))
}
return h('div', { style: { paddingLeft: pl } }, [
...keyFrag,
valueEl,
comma
])
}
}
})
export default defineComponent({
name: 'JsonViewer',
components: { JsonNode },
props: {
data: { required: true }
},
setup (props) {
const expandSignal = ref(0)
const collapseSignal = ref(0)
const expandMode = ref(null) // tracks last toolbar action for newly-mounted nodes
provide('jeExpand', expandSignal)
provide('jeCollapse', collapseSignal)
provide('jeExpandMode', expandMode)
const copyDone = ref(false)
const copyJson = () => {
const text = JSON.stringify(props.data, null, 2)
if (navigator.clipboard) {
navigator.clipboard.writeText(text).then(() => {
copyDone.value = true
setTimeout(() => { copyDone.value = false }, 1500)
})
}
}
return () => h('div', {
style: {
position: 'relative',
fontFamily: "'Consolas', 'Courier New', monospace",
fontSize: '12px',
lineHeight: '1.7',
backgroundColor: '#fafafa',
border: '1px solid #e0e0e0',
borderRadius: '4px',
maxHeight: '60vh',
overflow: 'auto',
padding: '10px 10px 10px 6px'
}
}, [
// Toolbar (top-right, sticky): Expand All · Collapse All · Copy
h('div', {
style: {
position: 'sticky',
top: 0,
float: 'right',
zIndex: 1,
marginBottom: '-20px',
display: 'flex',
gap: '4px'
}
}, [
h('span', {
onClick: () => { expandMode.value = 'expanded'; expandSignal.value++ },
title: 'Expand all nodes',
style: {
cursor: 'pointer',
fontSize: '11px',
padding: '2px 8px',
backgroundColor: '#e8e8e8',
border: '1px solid #ccc',
borderRadius: '3px',
color: '#555',
userSelect: 'none'
}
}, '⊞ Expand all'),
h('span', {
onClick: () => { expandMode.value = 'collapsed'; collapseSignal.value++ },
title: 'Collapse to default view (2 levels)',
style: {
cursor: 'pointer',
fontSize: '11px',
padding: '2px 8px',
backgroundColor: '#e8e8e8',
border: '1px solid #ccc',
borderRadius: '3px',
color: '#555',
userSelect: 'none'
}
}, '⊟ Collapse all'),
h('span', {
onClick: copyJson,
title: 'Copy JSON',
style: {
cursor: 'pointer',
fontSize: '11px',
padding: '2px 8px',
backgroundColor: copyDone.value ? '#d4edda' : '#e8e8e8',
border: '1px solid #ccc',
borderRadius: '3px',
color: copyDone.value ? '#155724' : '#555',
userSelect: 'none'
}
}, copyDone.value ? '✓ Copied' : 'Copy')
]),
// Tree root
h(JsonNode, { data: props.data, depth: 0, keyName: null, isLast: true })
])
}
})
</script>

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

@ -103,6 +103,7 @@ import { ref, reactive, toRaw, h } from 'vue'
import { getAPI, postAPI } from '@/api'
import TooltipLabel from '@/components/widgets/TooltipLabel'
import DetailsInput from '@/components/widgets/DetailsInput'
import JsonViewer from '@/views/extension/JsonViewer.vue'
export default {
name: 'RunCustomAction',
@ -277,75 +278,88 @@ export default {
} catch (_) {}
const modalType = success ? this.$success : this.$error
const contentElements = [h('p', `${message}`)]
if (extensionMessage && !Array.isArray(extensionMessage) && typeof extensionMessage === 'object' && Object.keys(extensionMessage).length > 0) {
extensionMessage = [extensionMessage]
}
if (Array.isArray(extensionMessage) && extensionMessage.length > 0 && printExtensionMessage) {
contentElements.push(
h('div', {
style: {
marginTop: '1em',
maxHeight: '50vh',
maxWidth: '100%',
overflow: 'auto',
backgroundColor: '#f6f6f6',
border: '1px solid #ddd',
borderRadius: '4px',
display: 'block'
}
}, [
h('table', {
if (printExtensionMessage && extensionMessage !== null && extensionMessage !== undefined) {
const isArray = Array.isArray(extensionMessage)
// A "flat" array has only primitive values safe to render as a table.
const isFlatArray = isArray && extensionMessage.length > 0 &&
extensionMessage.every(row =>
typeof row === 'object' && row !== null &&
Object.values(row).every(v => v === null || typeof v !== 'object')
)
if (isFlatArray) {
contentElements.push(
h('div', {
style: {
width: '100%',
minWidth: 'max-content',
borderCollapse: 'collapse',
whiteSpace: 'pre-wrap'
marginTop: '1em',
maxHeight: '50vh',
maxWidth: '100%',
overflow: 'auto',
backgroundColor: '#f6f6f6',
border: '1px solid #ddd',
borderRadius: '4px',
display: 'block'
}
}, [
h('thead', [
h('tr', Object.keys(extensionMessage[0]).map(key =>
h('th', {
style: {
padding: '8px',
border: '1px solid #ddd',
textAlign: 'left',
fontWeight: 'bold',
backgroundColor: '#fafafa'
}
}, key)
h('table', {
style: {
width: '100%',
minWidth: 'max-content',
borderCollapse: 'collapse',
whiteSpace: 'pre-wrap'
}
}, [
h('thead', [
h('tr', Object.keys(extensionMessage[0]).map(key =>
h('th', {
style: {
padding: '8px',
border: '1px solid #ddd',
textAlign: 'left',
fontWeight: 'bold',
backgroundColor: '#fafafa'
}
}, key)
))
]),
h('tbody', extensionMessage.map(row =>
h('tr', Object.values(row).map(value =>
h('td', {
style: {
padding: '8px',
border: '1px solid #ddd',
fontFamily: 'monospace'
}
}, String(value))
))
))
]),
h('tbody', extensionMessage.map(row =>
h('tr', Object.values(row).map(value =>
h('td', {
style: {
padding: '8px',
border: '1px solid #ddd',
fontFamily: 'monospace'
}
}, String(value))
))
))
])
])
])
)
} else if (printExtensionMessage === 'true') {
contentElements.push(
h('div', {
style: {
marginTop: '1em',
maxHeight: '50vh',
padding: '10px',
overflowY: 'auto',
backgroundColor: '#f6f6f6',
border: '1px solid #ddd',
borderRadius: '4px',
fontFamily: 'monospace',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word'
}
}, String(extensionMessage))
)
)
} else if (typeof extensionMessage === 'string') {
contentElements.push(
h('div', {
style: {
marginTop: '1em',
maxHeight: '50vh',
padding: '10px',
overflowY: 'auto',
backgroundColor: '#f6f6f6',
border: '1px solid #ddd',
borderRadius: '4px',
fontFamily: 'monospace',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word'
}
}, extensionMessage)
)
} else {
// Complex object or non-flat array render as interactive JSON tree.
contentElements.push(
h('div', { style: { marginTop: '1em' } }, [
h(JsonViewer, { data: extensionMessage })
])
)
}
}
modalType({

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

@ -706,6 +706,7 @@ export default {
}
this.loading = true
getAPI('listExtensions', {
type: 'Orchestrator'
}).then(response => {
this.extensionsList = response.listextensionsresponse.extension || []
}).catch(error => {

View File

@ -302,6 +302,7 @@ export default {
fetchExtensionsList () {
this.loading = true
getAPI('listExtensions', {
type: 'Orchestrator'
}).then(response => {
this.extensionsList = response.listextensionsresponse.extension || []
}).catch(error => {

View File

@ -22,6 +22,7 @@
: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 +42,29 @@
: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>
<div v-if="showFormAction">
<keep-alive v-if="currentAction.component">
<a-modal
@ -170,7 +192,8 @@ export default {
actionLoading: false,
showFormAction: false,
currentAction: {},
tabKey: 'BaremetalDhcpProvider'
tabKey: 'BaremetalDhcpProvider',
registeredExtensions: []
}
},
computed: {
@ -848,7 +871,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' }
@ -1148,6 +1171,53 @@ export default {
return
}
this.fetchServiceProvider()
this.fetchRegisteredExtensions()
},
fetchRegisteredExtensions () {
// Load NetworkOrchestrator extensions registered to this physical network
getAPI('listExtensions', {
type: 'NetworkOrchestrator',
details: 'resource'
}).then(json => {
const exts = (json?.listextensionsresponse?.extension) || []
this.registeredExtensions = exts.filter(ext =>
(ext.resources || []).some(r => r.type === 'PhysicalNetwork' && r.id === this.resource.id)
)
}).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 }">
@ -86,6 +86,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>

View File

@ -405,6 +405,9 @@
<a-tab-pane :tab="$t('label.vnf.appliances')" key="vnf" v-if="'deployVnfAppliance' in $store.getters.apis">
<VnfAppliancesTab :resource="resource" :loading="loading" />
</a-tab-pane>
<a-tab-pane :tab="$t('label.custom.actions')" key="customactions" v-if="'runCustomAction' in $store.getters.apis && 'listCustomActions' in $store.getters.apis && resource.service && resource.service.some(s => s.name === 'CustomAction')">
<RunCustomAction :resource="resource" />
</a-tab-pane>
<a-tab-pane :tab="$t('label.events')" key="events" v-if="'listEvents' in $store.getters.apis">
<events-tab :resource="resource" resourceType="Vpc" :loading="loading" />
</a-tab-pane>
@ -433,6 +436,7 @@ import AnnotationsTab from '@/components/view/AnnotationsTab'
import ResourceIcon from '@/components/view/ResourceIcon'
import BgpPeersTab from '@/views/infra/zone/BgpPeersTab.vue'
import StaticRoutesTab from './StaticRoutesTab'
import RunCustomAction from '@/views/extension/RunCustomAction'
export default {
name: 'VpcTab',
@ -445,6 +449,7 @@ export default {
VpcTiersTab,
VnfAppliancesTab,
StaticRoutesTab,
RunCustomAction,
EventsTab,
AnnotationsTab,
ResourceIcon

View File

@ -739,6 +739,23 @@ export default {
isSupportedServiceObject (obj) {
return (obj !== null && obj !== undefined && Object.keys(obj).length > 0 && obj.constructor === Object && 'provider' in obj)
},
isVpcCoreProvider (providerName, serviceName) {
if (['VpcVirtualRouter', 'Netscaler', 'BigSwitchBcf', 'ConfigDrive'].includes(providerName)) {
return true
}
return serviceName === 'Connectivity' && ['NiciraNvp', 'Ovs', 'JuniperContrailVpcRouter'].includes(providerName)
},
isBuiltInNetworkProvider (providerName) {
const builtInProviders = [
'VirtualRouter', 'JuniperContrailRouter', 'JuniperContrailVpcRouter', 'JuniperSRX', 'PaloAlto',
'F5BigIp', 'Netscaler', 'ExternalDhcpServer', 'ExternalGateWay', 'ElasticLoadBalancerVm',
'SecurityGroupProvider', 'VpcVirtualRouter', 'None', 'NiciraNvp', 'InternalLbVm', 'CiscoVnmc',
'Ovs', 'Opendaylight', 'BrocadeVcs', 'GloboDns', 'BigSwitchBcf', 'ConfigDrive', 'Tungsten',
'Nsx', 'Netris', 'BaremetalDhcpProvider', 'BaremetalPxeProvider', 'BaremetalUserdataProvider',
'StratosphereSsp'
]
return builtInProviders.includes(providerName)
},
fetchDomainData () {
const params = {}
params.listAll = true
@ -854,6 +871,9 @@ export default {
for (var j in this.supportedServices[i].provider) {
var provider = this.supportedServices[i].provider[j]
provider.description = provider.name
provider.displaytext = this.isBuiltInNetworkProvider(provider.name)
? provider.name
: `${provider.name} (${this.$t('label.extension')})`
provider.enabled = true
if (provider.name === 'VpcVirtualRouter') {
provider.enabled = false
@ -917,11 +937,15 @@ export default {
var providers = svc.provider
providers.forEach(function (provider, providerIndex) {
if (self.forVpc) { // *** vpc ***
var enabledProviders = ['VpcVirtualRouter', 'Netscaler', 'BigSwitchBcf', 'ConfigDrive']
if (self.lbType === 'internalLb') {
enabledProviders.push('InternalLbVm')
// Keep the known VPC-safe providers allowlisted and only additionally enable
// extension providers, which listSupportedNetworkServices() already only returns
// for a service once the extension is confirmed to support it.
if (provider.name === 'InternalLbVm') {
provider.enabled = self.lbType === 'internalLb' && svc.name === 'Lb'
} else {
provider.enabled = self.isVpcCoreProvider(provider.name, svc.name) ||
!self.isBuiltInNetworkProvider(provider.name)
}
provider.enabled = enabledProviders.includes(provider.name)
} else { // *** non-vpc ***
provider.enabled = !['InternalLbVm', 'VpcVirtualRouter', 'Nsx', 'Netris'].includes(provider.name)
}

View File

@ -154,7 +154,7 @@
:checkBoxLabel="item.description"
:forExternalNetProvider="form.provider === 'NSX' || form.provider === 'Netris'"
:defaultCheckBoxValue="form.provider === 'NSX' || form.provider === 'Netris'"
:selectOptions="item.provider"
:selectOptions="item.provider || []"
@handle-checkselectpair-change="handleSupportedServiceChange"/>
</a-list-item>
</template>
@ -431,6 +431,23 @@ export default {
this.zoneLoading = false
})
},
isVpcCoreProvider (providerName, serviceName) {
if (['VpcVirtualRouter', 'Netscaler', 'BigSwitchBcf', 'ConfigDrive'].includes(providerName)) {
return true
}
return serviceName === 'Connectivity' && ['NiciraNvp', 'Ovs', 'JuniperContrailVpcRouter'].includes(providerName)
},
isBuiltInNetworkProvider (providerName) {
const builtInProviders = [
'VirtualRouter', 'JuniperContrailRouter', 'JuniperContrailVpcRouter', 'JuniperSRX', 'PaloAlto',
'F5BigIp', 'Netscaler', 'ExternalDhcpServer', 'ExternalGateWay', 'ElasticLoadBalancerVm',
'SecurityGroupProvider', 'VpcVirtualRouter', 'None', 'NiciraNvp', 'InternalLbVm', 'CiscoVnmc',
'Ovs', 'Opendaylight', 'BrocadeVcs', 'GloboDns', 'BigSwitchBcf', 'ConfigDrive', 'Tungsten',
'Nsx', 'Netris', 'BaremetalDhcpProvider', 'BaremetalPxeProvider', 'BaremetalUserdataProvider',
'StratosphereSsp'
]
return builtInProviders.includes(providerName)
},
fetchSupportedServiceData () {
var services = []
if (this.provider === 'NSX') {
@ -520,82 +537,54 @@ export default {
provider: [{ name: 'VpcVirtualRouter' }]
})
} else {
services.push({
name: 'Dhcp',
provider: [
{ name: 'VpcVirtualRouter' },
{ name: 'ConfigDrive' }
]
})
services.push({
name: 'Dns',
provider: [
{ name: 'VpcVirtualRouter' },
{ name: 'ConfigDrive' }
]
})
services.push({
name: 'Lb',
provider: [
{ name: 'VpcVirtualRouter' },
{ name: 'InternalLbVm' }
]
})
services.push({
name: 'Gateway',
provider: [
{ name: 'VpcVirtualRouter' },
{ name: 'BigSwitchBcf' }
]
})
services.push({
name: 'StaticNat',
provider: [
{ name: 'VpcVirtualRouter' },
{ name: 'BigSwitchBcf' }
]
})
services.push({
name: 'SourceNat',
provider: [
{ name: 'VpcVirtualRouter' },
{ name: 'BigSwitchBcf' }
]
})
services.push({
name: 'NetworkACL',
provider: [
{ name: 'VpcVirtualRouter' },
{ name: 'BigSwitchBcf' }
]
})
services.push({
name: 'PortForwarding',
provider: [{ name: 'VpcVirtualRouter' }]
})
services.push({
name: 'UserData',
provider: [
{ name: 'VpcVirtualRouter' },
{ name: 'ConfigDrive' }
]
})
services.push({
name: 'Vpn',
provider: [
{ name: 'VpcVirtualRouter' },
{ name: 'BigSwitchBcf' }
]
})
services.push({
name: 'Connectivity',
provider: [
{ name: 'BigSwitchBcf' },
{ name: 'NiciraNvp' },
{ name: 'Ovs' },
{ name: 'JuniperContrailVpcRouter' }
]
this.supportedServices = []
this.supportedServiceLoading = true
getAPI('listSupportedNetworkServices').then(json => {
const vpcServices = ['Dhcp', 'Dns', 'Lb', 'Gateway', 'StaticNat', 'SourceNat', 'NetworkACL', 'PortForwarding', 'UserData', 'Vpn', 'Connectivity', 'CustomAction']
services = (json?.listsupportednetworkservicesresponse?.networkservice || [])
.filter(service => vpcServices.includes(service.name))
.map(service => {
const providerMap = {}
const providers = [...(service.provider || []), ...(service.name === 'Lb' ? [{ name: 'InternalLbVm' }] : [])]
.map(provider => {
const providerName = provider.name === 'VirtualRouter' ? 'VpcVirtualRouter' : provider.name
const isExtension = !this.isBuiltInNetworkProvider(providerName)
const enabled = providerName === 'InternalLbVm'
? service.name === 'Lb'
: this.isVpcCoreProvider(providerName, service.name) || isExtension
return {
name: providerName,
description: providerName,
displaytext: isExtension ? `${providerName} (${this.$t('label.extension')})` : providerName,
enabled
}
})
.filter(provider => {
if (providerMap[provider.name]) {
return false
}
providerMap[provider.name] = true
return true
})
return {
...service,
description: service.name,
provider: providers
}
})
this.supportedServices = []
if (this.networkmode === 'ROUTED') {
services = services.filter(service => !['SourceNat', 'StaticNat', 'Lb', 'PortForwarding', 'Vpn'].includes(service.name))
}
this.supportedServices = services
}).catch(error => {
this.supportedServices = []
this.$notifyError(error)
}).finally(() => {
this.supportedServiceLoading = false
})
return
}
this.supportedServices = []
if (this.networkmode === 'ROUTED') {
@ -647,7 +636,7 @@ export default {
if (service === 'SourceNat') {
this.sourceNatServiceChecked = checked
}
if (checked && provider != null & provider !== undefined) {
if (checked && provider != null && provider !== undefined) {
this.selectedServiceProviderMap[service] = provider
} else {
delete this.selectedServiceProviderMap[service]