diff options
author | Ashlee Young <ashlee@onosfw.com> | 2015-09-09 22:15:21 -0700 |
---|---|---|
committer | Ashlee Young <ashlee@onosfw.com> | 2015-09-09 22:15:21 -0700 |
commit | 13d05bc8458758ee39cb829098241e89616717ee (patch) | |
tree | 22a4d1ce65f15952f07a3df5af4b462b4697cb3a /framework/src/onos/apps/sdnip | |
parent | 6139282e1e93c2322076de4b91b1c85d0bc4a8b3 (diff) |
ONOS checkin based on commit tag e796610b1f721d02f9b0e213cf6f7790c10ecd60
Change-Id: Ife8810491034fe7becdba75dda20de4267bd15cd
Diffstat (limited to 'framework/src/onos/apps/sdnip')
16 files changed, 3169 insertions, 0 deletions
diff --git a/framework/src/onos/apps/sdnip/app.xml b/framework/src/onos/apps/sdnip/app.xml new file mode 100644 index 00000000..7aafa6ab --- /dev/null +++ b/framework/src/onos/apps/sdnip/app.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ Copyright 2015 Open Networking Laboratory + ~ + ~ Licensed 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. + --> +<app name="org.onosproject.sdnip" origin="ON.Lab" version="${project.version}" + featuresRepo="mvn:${project.groupId}/${project.artifactId}/${project.version}/xml/features" + features="${project.artifactId}"> + <description>${project.description}</description> + <artifact>mvn:${project.groupId}/${project.artifactId}/${project.version}</artifact> + <artifact>mvn:${project.groupId}/onos-app-routing-api/${project.version}</artifact> + <artifact>mvn:${project.groupId}/onos-app-routing/${project.version}</artifact> +</app> diff --git a/framework/src/onos/apps/sdnip/features.xml b/framework/src/onos/apps/sdnip/features.xml new file mode 100644 index 00000000..21ae89d3 --- /dev/null +++ b/framework/src/onos/apps/sdnip/features.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<!-- + ~ Copyright 2015 Open Networking Laboratory + ~ + ~ Licensed 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. + --> +<features xmlns="http://karaf.apache.org/xmlns/features/v1.2.0" name="${project.artifactId}-${project.version}"> + <repository>mvn:${project.groupId}/${project.artifactId}/${project.version}/xml/features</repository> + <feature name="onos-app-sdnip" version="${project.version}" + description="${project.description}"> + <feature>onos-api</feature> + <bundle>mvn:${project.groupId}/onos-app-sdnip/${project.version}</bundle> + <bundle>mvn:${project.groupId}/onos-app-routing-api/${project.version}</bundle> + <bundle>mvn:${project.groupId}/onos-app-routing/${project.version}</bundle> + </feature> +</features> diff --git a/framework/src/onos/apps/sdnip/pom.xml b/framework/src/onos/apps/sdnip/pom.xml new file mode 100644 index 00000000..68b500d5 --- /dev/null +++ b/framework/src/onos/apps/sdnip/pom.xml @@ -0,0 +1,94 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ Copyright 2014 Open Networking Laboratory + ~ + ~ Licensed 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. + --> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.onosproject</groupId> + <artifactId>onos-apps</artifactId> + <version>1.3.0-SNAPSHOT</version> + <relativePath>../pom.xml</relativePath> + </parent> + + <artifactId>onos-app-sdnip</artifactId> + <packaging>bundle</packaging> + + <description>SDN-IP peering application</description> + + <dependencies> + <dependency> + <groupId>com.google.guava</groupId> + <artifactId>guava</artifactId> + </dependency> + + <dependency> + <groupId>org.onosproject</groupId> + <artifactId>onlab-misc</artifactId> + </dependency> + + <dependency> + <groupId>org.onosproject</groupId> + <artifactId>onlab-junit</artifactId> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>org.onosproject</groupId> + <artifactId>onos-api</artifactId> + <version>${project.version}</version> + <scope>test</scope> + <classifier>tests</classifier> + </dependency> + + <dependency> + <groupId>org.onosproject</groupId> + <artifactId>onos-app-routing</artifactId> + <version>${project.version}</version> + </dependency> + + <dependency> + <groupId>org.onosproject</groupId> + <artifactId>onos-cli</artifactId> + <version>${project.version}</version> + </dependency> + + <dependency> + <groupId>org.onosproject</groupId> + <artifactId>onos-app-config</artifactId> + <version>${project.version}</version> + </dependency> + + <dependency> + <groupId>org.apache.karaf.shell</groupId> + <artifactId>org.apache.karaf.shell.console</artifactId> + </dependency> + + <dependency> + <groupId>org.osgi</groupId> + <artifactId>org.osgi.core</artifactId> + </dependency> + + <dependency> + <groupId>org.easymock</groupId> + <artifactId>easymock</artifactId> + <scope>test</scope> + </dependency> + </dependencies> + +</project> diff --git a/framework/src/onos/apps/sdnip/src/main/java/org/onosproject/sdnip/IntentSynchronizer.java b/framework/src/onos/apps/sdnip/src/main/java/org/onosproject/sdnip/IntentSynchronizer.java new file mode 100644 index 00000000..35ff7054 --- /dev/null +++ b/framework/src/onos/apps/sdnip/src/main/java/org/onosproject/sdnip/IntentSynchronizer.java @@ -0,0 +1,980 @@ +/* + * Copyright 2014-2015 Open Networking Laboratory + * + * Licensed 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.onosproject.sdnip; + +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import org.onlab.packet.Ethernet; +import org.onlab.packet.IpAddress; +import org.onlab.packet.IpPrefix; +import org.onlab.packet.MacAddress; +import org.onlab.packet.VlanId; +import org.onosproject.core.ApplicationId; +import org.onosproject.incubator.net.intf.Interface; +import org.onosproject.incubator.net.intf.InterfaceService; +import org.onosproject.net.ConnectPoint; +import org.onosproject.net.Host; +import org.onosproject.net.flow.DefaultTrafficSelector; +import org.onosproject.net.flow.DefaultTrafficTreatment; +import org.onosproject.net.flow.TrafficSelector; +import org.onosproject.net.flow.TrafficTreatment; +import org.onosproject.net.flow.criteria.Criterion; +import org.onosproject.net.flow.criteria.IPCriterion; +import org.onosproject.net.host.HostService; +import org.onosproject.net.intent.Constraint; +import org.onosproject.net.intent.Intent; +import org.onosproject.net.intent.IntentService; +import org.onosproject.net.intent.IntentState; +import org.onosproject.net.intent.Key; +import org.onosproject.net.intent.MultiPointToSinglePointIntent; +import org.onosproject.net.intent.PointToPointIntent; +import org.onosproject.net.intent.constraint.PartialFailureConstraint; +import org.onosproject.routing.FibListener; +import org.onosproject.routing.FibUpdate; +import org.onosproject.routing.IntentRequestListener; +import org.onosproject.routing.config.RoutingConfigurationService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Semaphore; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Synchronizes intents between the in-memory intent store and the + * IntentService. + */ +public class IntentSynchronizer implements FibListener, IntentRequestListener { + private static final int PRIORITY_OFFSET = 100; + private static final int PRIORITY_MULTIPLIER = 5; + protected static final ImmutableList<Constraint> CONSTRAINTS + = ImmutableList.of(new PartialFailureConstraint()); + + private static final Logger log = + LoggerFactory.getLogger(IntentSynchronizer.class); + + private final ApplicationId appId; + private final IntentService intentService; + private final HostService hostService; + private final InterfaceService interfaceService; + private final Map<IntentKey, PointToPointIntent> peerIntents; + private final Map<IpPrefix, MultiPointToSinglePointIntent> routeIntents; + + // + // State to deal with SDN-IP Leader election and pushing Intents + // + private final ExecutorService bgpIntentsSynchronizerExecutor; + private final Semaphore intentsSynchronizerSemaphore = new Semaphore(0); + private volatile boolean isElectedLeader = false; + private volatile boolean isActivatedLeader = false; + + private final RoutingConfigurationService configService; + + /** + * Class constructor. + * + * @param appId the Application ID + * @param intentService the intent service + * @param hostService the host service + * @param configService the SDN-IP configuration service + * @param interfaceService the interface service + */ + IntentSynchronizer(ApplicationId appId, IntentService intentService, + HostService hostService, + RoutingConfigurationService configService, + InterfaceService interfaceService) { + this.appId = appId; + this.intentService = intentService; + this.hostService = hostService; + this.interfaceService = interfaceService; + peerIntents = new ConcurrentHashMap<>(); + routeIntents = new ConcurrentHashMap<>(); + + this.configService = configService; + + bgpIntentsSynchronizerExecutor = Executors.newSingleThreadExecutor( + new ThreadFactoryBuilder() + .setNameFormat("sdnip-intents-synchronizer-%d").build()); + } + + /** + * Starts the synchronizer. + */ + public void start() { + bgpIntentsSynchronizerExecutor.execute(this::doIntentSynchronizationThread); + } + + /** + * Stops the synchronizer. + */ + public void stop() { + synchronized (this) { + // Stop the thread(s) + bgpIntentsSynchronizerExecutor.shutdownNow(); + + // + // Withdraw all SDN-IP intents + // + if (!isElectedLeader) { + return; // Nothing to do: not the leader anymore + } + + // + // NOTE: We don't withdraw the intents during shutdown, because + // it creates flux in the data plane during switchover. + // + + /* + // + // Build a batch operation to withdraw all intents from this + // application. + // + log.debug("SDN-IP Intent Synchronizer shutdown: " + + "withdrawing all intents..."); + IntentOperations.Builder builder = IntentOperations.builder(appId); + for (Intent intent : intentService.getIntents()) { + // Skip the intents from other applications + if (!intent.appId().equals(appId)) { + continue; + } + + // Skip the intents that are already withdrawn + IntentState intentState = + intentService.getIntentState(intent.id()); + if ((intentState == null) || + intentState.equals(IntentState.WITHDRAWING) || + intentState.equals(IntentState.WITHDRAWN)) { + continue; + } + + log.trace("SDN-IP Intent Synchronizer withdrawing intent: {}", + intent); + builder.addWithdrawOperation(intent.id()); + } + IntentOperations intentOperations = builder.build(); + intentService.execute(intentOperations); + leaderChanged(false); + + peerIntents.clear(); + routeIntents.clear(); + log.debug("SDN-IP Intent Synchronizer shutdown completed"); + */ + } + } + + /** + * Signals the synchronizer that the SDN-IP leadership has changed. + * + * @param isLeader true if this instance is now the leader, otherwise false + */ + public void leaderChanged(boolean isLeader) { + log.debug("SDN-IP Leader changed: {}", isLeader); + + if (!isLeader) { + this.isElectedLeader = false; + this.isActivatedLeader = false; + return; // Nothing to do + } + this.isActivatedLeader = false; + this.isElectedLeader = true; + + // + // Tell the Intents Synchronizer thread to start the synchronization + // + intentsSynchronizerSemaphore.release(); + } + + /** + * Gets the route intents. + * + * @return the route intents + */ + public Collection<MultiPointToSinglePointIntent> getRouteIntents() { + List<MultiPointToSinglePointIntent> result = new LinkedList<>(); + + for (Map.Entry<IpPrefix, MultiPointToSinglePointIntent> entry : + routeIntents.entrySet()) { + result.add(entry.getValue()); + } + return result; + } + + /** + * Thread for Intent Synchronization. + */ + private void doIntentSynchronizationThread() { + boolean interrupted = false; + try { + while (!interrupted) { + try { + intentsSynchronizerSemaphore.acquire(); + // + // Drain all permits, because a single synchronization is + // sufficient. + // + intentsSynchronizerSemaphore.drainPermits(); + } catch (InterruptedException e) { + interrupted = true; + break; + } + synchronizeIntents(); + } + } finally { + if (interrupted) { + Thread.currentThread().interrupt(); + } + } + } + + /** + * Submits a collection of point-to-point intents. + * + * @param intents the intents to submit + */ + void submitPeerIntents(Collection<PointToPointIntent> intents) { + synchronized (this) { + // Store the intents in memory + for (PointToPointIntent intent : intents) { + peerIntents.put(new IntentKey(intent), intent); + } + + // Push the intents + if (isElectedLeader && isActivatedLeader) { + log.debug("SDN-IP Submitting all Peer Intents..."); + for (Intent intent : intents) { + log.trace("SDN-IP Submitting intents: {}", intent); + intentService.submit(intent); + } + } + } + } + + /** + * Submits a MultiPointToSinglePointIntent for reactive routing. + * + * @param ipPrefix the IP prefix to match in a MultiPointToSinglePointIntent + * @param intent the intent to submit + */ + void submitReactiveIntent(IpPrefix ipPrefix, MultiPointToSinglePointIntent intent) { + synchronized (this) { + // Store the intent in memory + routeIntents.put(ipPrefix, intent); + + // Push the intent + if (isElectedLeader && isActivatedLeader) { + log.trace("SDN-IP submitting reactive routing intent: {}", intent); + intentService.submit(intent); + } + } + } + + /** + * Generates a route intent for a prefix, the next hop IP address, and + * the next hop MAC address. + * <p/> + * This method will find the egress interface for the intent. + * Intent will match dst IP prefix and rewrite dst MAC address at all other + * border switches, then forward packets according to dst MAC address. + * + * @param prefix IP prefix of the route to add + * @param nextHopIpAddress IP address of the next hop + * @param nextHopMacAddress MAC address of the next hop + * @return the generated intent, or null if no intent should be submitted + */ + private MultiPointToSinglePointIntent generateRouteIntent( + IpPrefix prefix, + IpAddress nextHopIpAddress, + MacAddress nextHopMacAddress) { + + // Find the attachment point (egress interface) of the next hop + Interface egressInterface = interfaceService.getMatchingInterface(nextHopIpAddress); + if (egressInterface == null) { + log.warn("No outgoing interface found for {}", + nextHopIpAddress); + return null; + } + + // + // Generate the intent itself + // + Set<ConnectPoint> ingressPorts = new HashSet<>(); + ConnectPoint egressPort = egressInterface.connectPoint(); + log.debug("Generating intent for prefix {}, next hop mac {}", + prefix, nextHopMacAddress); + + for (Interface intf : interfaceService.getInterfaces()) { + // TODO this should be only peering interfaces + if (!intf.connectPoint().equals(egressInterface.connectPoint())) { + ConnectPoint srcPort = intf.connectPoint(); + ingressPorts.add(srcPort); + } + } + + // Match the destination IP prefix at the first hop + TrafficSelector.Builder selector = DefaultTrafficSelector.builder(); + if (prefix.isIp4()) { + selector.matchEthType(Ethernet.TYPE_IPV4); + selector.matchIPDst(prefix); + } else { + selector.matchEthType(Ethernet.TYPE_IPV6); + selector.matchIPv6Dst(prefix); + } + + // Rewrite the destination MAC address + TrafficTreatment.Builder treatment = DefaultTrafficTreatment.builder() + .setEthDst(nextHopMacAddress); + if (!egressInterface.vlan().equals(VlanId.NONE)) { + treatment.setVlanId(egressInterface.vlan()); + // If we set VLAN ID, we have to make sure a VLAN tag exists. + // TODO support no VLAN -> VLAN routing + selector.matchVlanId(VlanId.ANY); + } + + int priority = + prefix.prefixLength() * PRIORITY_MULTIPLIER + PRIORITY_OFFSET; + Key key = Key.of(prefix.toString(), appId); + return MultiPointToSinglePointIntent.builder() + .appId(appId) + .key(key) + .selector(selector.build()) + .treatment(treatment.build()) + .ingressPoints(ingressPorts) + .egressPoint(egressPort) + .priority(priority) + .constraints(CONSTRAINTS) + .build(); + } + + @Override + public void setUpConnectivityInternetToHost(IpAddress hostIpAddress) { + checkNotNull(hostIpAddress); + Set<ConnectPoint> ingressPoints = + configService.getBgpPeerConnectPoints(); + + TrafficSelector.Builder selector = DefaultTrafficSelector.builder(); + + if (hostIpAddress.isIp4()) { + selector.matchEthType(Ethernet.TYPE_IPV4); + } else { + selector.matchEthType(Ethernet.TYPE_IPV6); + } + + // Match the destination IP prefix at the first hop + IpPrefix ipPrefix = hostIpAddress.toIpPrefix(); + selector.matchIPDst(ipPrefix); + + // Rewrite the destination MAC address + MacAddress hostMac = null; + ConnectPoint egressPoint = null; + for (Host host : hostService.getHostsByIp(hostIpAddress)) { + if (host.mac() != null) { + hostMac = host.mac(); + egressPoint = host.location(); + break; + } + } + if (hostMac == null) { + hostService.startMonitoringIp(hostIpAddress); + return; + } + + TrafficTreatment.Builder treatment = + DefaultTrafficTreatment.builder().setEthDst(hostMac); + Key key = Key.of(ipPrefix.toString(), appId); + int priority = ipPrefix.prefixLength() * PRIORITY_MULTIPLIER + + PRIORITY_OFFSET; + MultiPointToSinglePointIntent intent = + MultiPointToSinglePointIntent.builder() + .appId(appId) + .key(key) + .selector(selector.build()) + .treatment(treatment.build()) + .ingressPoints(ingressPoints) + .egressPoint(egressPoint) + .priority(priority) + .constraints(CONSTRAINTS) + .build(); + + log.trace("Generates ConnectivityInternetToHost intent {}", intent); + submitReactiveIntent(ipPrefix, intent); + } + + + @Override + public void update(Collection<FibUpdate> updates, Collection<FibUpdate> withdraws) { + // + // NOTE: Semantically, we MUST withdraw existing intents before + // submitting new intents. + // + synchronized (this) { + MultiPointToSinglePointIntent intent; + + log.debug("SDN-IP submitting intents = {} withdrawing = {}", + updates.size(), withdraws.size()); + + // + // Prepare the Intent batch operations for the intents to withdraw + // + for (FibUpdate withdraw : withdraws) { + checkArgument(withdraw.type() == FibUpdate.Type.DELETE, + "FibUpdate with wrong type in withdraws list"); + + IpPrefix prefix = withdraw.entry().prefix(); + intent = routeIntents.remove(prefix); + if (intent == null) { + log.trace("SDN-IP No intent in routeIntents to delete " + + "for prefix: {}", prefix); + continue; + } + if (isElectedLeader && isActivatedLeader) { + log.trace("SDN-IP Withdrawing intent: {}", intent); + intentService.withdraw(intent); + } + } + + // + // Prepare the Intent batch operations for the intents to submit + // + for (FibUpdate update : updates) { + checkArgument(update.type() == FibUpdate.Type.UPDATE, + "FibUpdate with wrong type in updates list"); + + IpPrefix prefix = update.entry().prefix(); + intent = generateRouteIntent(prefix, update.entry().nextHopIp(), + update.entry().nextHopMac()); + + if (intent == null) { + // This preserves the old semantics - if an intent can't be + // generated, we don't do anything with that prefix. But + // perhaps we should withdraw the old intent anyway? + continue; + } + + MultiPointToSinglePointIntent oldIntent = + routeIntents.put(prefix, intent); + if (isElectedLeader && isActivatedLeader) { + if (oldIntent != null) { + log.trace("SDN-IP Withdrawing old intent: {}", + oldIntent); + intentService.withdraw(oldIntent); + } + log.trace("SDN-IP Submitting intent: {}", intent); + intentService.submit(intent); + } + } + } + } + + /** + * Synchronize the in-memory Intents with the Intents in the Intent + * framework. + */ + void synchronizeIntents() { + synchronized (this) { + + Map<IntentKey, Intent> localIntents = new HashMap<>(); + Map<IntentKey, Intent> fetchedIntents = new HashMap<>(); + Collection<Intent> storeInMemoryIntents = new LinkedList<>(); + Collection<Intent> addIntents = new LinkedList<>(); + Collection<Intent> deleteIntents = new LinkedList<>(); + + if (!isElectedLeader) { + return; // Nothing to do: not the leader anymore + } + log.debug("SDN-IP synchronizing all intents..."); + + // Prepare the local intents + for (Intent intent : routeIntents.values()) { + localIntents.put(new IntentKey(intent), intent); + } + for (Intent intent : peerIntents.values()) { + localIntents.put(new IntentKey(intent), intent); + } + + // Fetch all intents for this application + for (Intent intent : intentService.getIntents()) { + if (!intent.appId().equals(appId)) { + continue; + } + fetchedIntents.put(new IntentKey(intent), intent); + } + if (log.isDebugEnabled()) { + for (Intent intent: fetchedIntents.values()) { + log.trace("SDN-IP Intent Synchronizer: fetched intent: {}", + intent); + } + } + + computeIntentsDelta(localIntents, fetchedIntents, + storeInMemoryIntents, addIntents, + deleteIntents); + + // + // Perform the actions: + // 1. Store in memory fetched intents that are same. Can be done + // even if we are not the leader anymore + // 2. Delete intents: check if the leader before the operation + // 3. Add intents: check if the leader before the operation + // + for (Intent intent : storeInMemoryIntents) { + // Store the intent in memory based on its type + if (intent instanceof MultiPointToSinglePointIntent) { + MultiPointToSinglePointIntent mp2pIntent = + (MultiPointToSinglePointIntent) intent; + // Find the IP prefix + Criterion c = + mp2pIntent.selector().getCriterion(Criterion.Type.IPV4_DST); + if (c == null) { + // Try IPv6 + c = + mp2pIntent.selector().getCriterion(Criterion.Type.IPV6_DST); + } + if (c != null && c instanceof IPCriterion) { + IPCriterion ipCriterion = (IPCriterion) c; + IpPrefix ipPrefix = ipCriterion.ip(); + if (ipPrefix == null) { + continue; + } + log.trace("SDN-IP Intent Synchronizer: updating " + + "in-memory Route Intent for prefix {}", + ipPrefix); + routeIntents.put(ipPrefix, mp2pIntent); + } else { + log.warn("SDN-IP no IPV4_DST or IPV6_DST criterion found for Intent {}", + mp2pIntent.id()); + } + continue; + } + if (intent instanceof PointToPointIntent) { + PointToPointIntent p2pIntent = (PointToPointIntent) intent; + log.trace("SDN-IP Intent Synchronizer: updating " + + "in-memory Peer Intent {}", p2pIntent); + peerIntents.put(new IntentKey(intent), p2pIntent); + continue; + } + } + + // Withdraw Intents + for (Intent intent : deleteIntents) { + intentService.withdraw(intent); + log.trace("SDN-IP Intent Synchronizer: withdrawing intent: {}", + intent); + } + if (!isElectedLeader) { + log.trace("SDN-IP Intent Synchronizer: cannot withdraw intents: " + + "not elected leader anymore"); + isActivatedLeader = false; + return; + } + + // Add Intents + for (Intent intent : addIntents) { + intentService.submit(intent); + log.trace("SDN-IP Intent Synchronizer: submitting intent: {}", + intent); + } + if (!isElectedLeader) { + log.trace("SDN-IP Intent Synchronizer: cannot submit intents: " + + "not elected leader anymore"); + isActivatedLeader = false; + return; + } + + if (isElectedLeader) { + isActivatedLeader = true; // Allow push of Intents + } else { + isActivatedLeader = false; + } + log.debug("SDN-IP intent synchronization completed"); + } + } + + /** + * Computes the delta in two sets of Intents: local in-memory Intents, + * and intents fetched from the Intent framework. + * + * @param localIntents the local in-memory Intents + * @param fetchedIntents the Intents fetched from the Intent framework + * @param storeInMemoryIntents the Intents that should be stored in memory. + * Note: This Collection must be allocated by the caller, and it will + * be populated by this method. + * @param addIntents the Intents that should be added to the Intent + * framework. Note: This Collection must be allocated by the caller, and + * it will be populated by this method. + * @param deleteIntents the Intents that should be deleted from the Intent + * framework. Note: This Collection must be allocated by the caller, and + * it will be populated by this method. + */ + private void computeIntentsDelta( + final Map<IntentKey, Intent> localIntents, + final Map<IntentKey, Intent> fetchedIntents, + Collection<Intent> storeInMemoryIntents, + Collection<Intent> addIntents, + Collection<Intent> deleteIntents) { + + // + // Compute the deltas between the LOCAL in-memory Intents and the + // FETCHED Intents: + // - If an Intent is in both the LOCAL and FETCHED sets: + // If the FETCHED Intent is WITHDRAWING or WITHDRAWN, then + // the LOCAL Intent should be added/installed; otherwise the + // FETCHED intent should be stored in the local memory + // (i.e., override the LOCAL Intent) to preserve the original + // Intent ID. + // - if a LOCAL Intent is not in the FETCHED set, then the LOCAL + // Intent should be added/installed. + // - If a FETCHED Intent is not in the LOCAL set, then the FETCHED + // Intent should be deleted/withdrawn. + // + for (Map.Entry<IntentKey, Intent> entry : localIntents.entrySet()) { + IntentKey intentKey = entry.getKey(); + Intent localIntent = entry.getValue(); + Intent fetchedIntent = fetchedIntents.get(intentKey); + + if (fetchedIntent == null) { + // + // No FETCHED Intent found: push the LOCAL Intent. + // + addIntents.add(localIntent); + continue; + } + + IntentState state = + intentService.getIntentState(fetchedIntent.key()); + if (state == null || + state == IntentState.WITHDRAWING || + state == IntentState.WITHDRAWN) { + // The intent has been withdrawn but according to our route + // table it should be installed. We'll reinstall it. + addIntents.add(localIntent); + continue; + } + storeInMemoryIntents.add(fetchedIntent); + } + + for (Map.Entry<IntentKey, Intent> entry : fetchedIntents.entrySet()) { + IntentKey intentKey = entry.getKey(); + Intent fetchedIntent = entry.getValue(); + Intent localIntent = localIntents.get(intentKey); + + if (localIntent != null) { + continue; + } + + IntentState state = + intentService.getIntentState(fetchedIntent.key()); + if (state == null || + state == IntentState.WITHDRAWING || + state == IntentState.WITHDRAWN) { + // Nothing to do. The intent has been already withdrawn. + continue; + } + // + // No LOCAL Intent found: delete/withdraw the FETCHED Intent. + // + deleteIntents.add(fetchedIntent); + } + } + + /** + * Helper class that can be used to compute the key for an Intent by + * by excluding the Intent ID. + */ + static final class IntentKey { + private final Intent intent; + + /** + * Constructor. + * + * @param intent the intent to use + */ + IntentKey(Intent intent) { + checkArgument((intent instanceof MultiPointToSinglePointIntent) || + (intent instanceof PointToPointIntent), + "Intent type not recognized", intent); + this.intent = intent; + } + + /** + * Compares two Multi-Point to Single-Point Intents whether they + * represent same logical intention. + * + * @param intent1 the first Intent to compare + * @param intent2 the second Intent to compare + * @return true if both Intents represent same logical intention, + * otherwise false + */ + static boolean equalIntents(MultiPointToSinglePointIntent intent1, + MultiPointToSinglePointIntent intent2) { + return Objects.equals(intent1.appId(), intent2.appId()) && + Objects.equals(intent1.selector(), intent2.selector()) && + Objects.equals(intent1.treatment(), intent2.treatment()) && + Objects.equals(intent1.ingressPoints(), intent2.ingressPoints()) && + Objects.equals(intent1.egressPoint(), intent2.egressPoint()); + } + + /** + * Compares two Point-to-Point Intents whether they represent + * same logical intention. + * + * @param intent1 the first Intent to compare + * @param intent2 the second Intent to compare + * @return true if both Intents represent same logical intention, + * otherwise false + */ + static boolean equalIntents(PointToPointIntent intent1, + PointToPointIntent intent2) { + return Objects.equals(intent1.appId(), intent2.appId()) && + Objects.equals(intent1.selector(), intent2.selector()) && + Objects.equals(intent1.treatment(), intent2.treatment()) && + Objects.equals(intent1.ingressPoint(), intent2.ingressPoint()) && + Objects.equals(intent1.egressPoint(), intent2.egressPoint()); + } + + @Override + public int hashCode() { + if (intent instanceof PointToPointIntent) { + PointToPointIntent p2pIntent = (PointToPointIntent) intent; + return Objects.hash(p2pIntent.appId(), + p2pIntent.resources(), + p2pIntent.selector(), + p2pIntent.treatment(), + p2pIntent.constraints(), + p2pIntent.ingressPoint(), + p2pIntent.egressPoint()); + } + if (intent instanceof MultiPointToSinglePointIntent) { + MultiPointToSinglePointIntent m2pIntent = + (MultiPointToSinglePointIntent) intent; + return Objects.hash(m2pIntent.appId(), + m2pIntent.resources(), + m2pIntent.selector(), + m2pIntent.treatment(), + m2pIntent.constraints(), + m2pIntent.ingressPoints(), + m2pIntent.egressPoint()); + } + checkArgument(false, "Intent type not recognized", intent); + return 0; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if ((obj == null) || (!(obj instanceof IntentKey))) { + return false; + } + IntentKey other = (IntentKey) obj; + + if (this.intent instanceof PointToPointIntent) { + if (!(other.intent instanceof PointToPointIntent)) { + return false; + } + return equalIntents((PointToPointIntent) this.intent, + (PointToPointIntent) other.intent); + } + if (this.intent instanceof MultiPointToSinglePointIntent) { + if (!(other.intent instanceof MultiPointToSinglePointIntent)) { + return false; + } + return equalIntents( + (MultiPointToSinglePointIntent) this.intent, + (MultiPointToSinglePointIntent) other.intent); + } + checkArgument(false, "Intent type not recognized", intent); + return false; + } + } + + @Override + public void setUpConnectivityHostToHost(IpAddress dstIpAddress, + IpAddress srcIpAddress, + MacAddress srcMacAddress, + ConnectPoint srcConnectPoint) { + checkNotNull(dstIpAddress); + checkNotNull(srcIpAddress); + checkNotNull(srcMacAddress); + checkNotNull(srcConnectPoint); + + IpPrefix srcIpPrefix = srcIpAddress.toIpPrefix(); + IpPrefix dstIpPrefix = dstIpAddress.toIpPrefix(); + ConnectPoint dstConnectPoint = null; + MacAddress dstMacAddress = null; + + for (Host host : hostService.getHostsByIp(dstIpAddress)) { + if (host.mac() != null) { + dstMacAddress = host.mac(); + dstConnectPoint = host.location(); + break; + } + } + if (dstMacAddress == null) { + hostService.startMonitoringIp(dstIpAddress); + return; + } + + // + // Handle intent from source host to destination host + // + MultiPointToSinglePointIntent srcToDstIntent = + hostToHostIntentGenerator(dstIpAddress, dstConnectPoint, + dstMacAddress, srcConnectPoint); + submitReactiveIntent(dstIpPrefix, srcToDstIntent); + + // + // Handle intent from destination host to source host + // + + // Since we proactively handle the intent from destination host to + // source host, we should check whether there is an exiting intent + // first. + if (mp2pIntentExists(srcIpPrefix)) { + updateExistingMp2pIntent(srcIpPrefix, dstConnectPoint); + return; + } else { + // There is no existing intent, create a new one. + MultiPointToSinglePointIntent dstToSrcIntent = + hostToHostIntentGenerator(srcIpAddress, srcConnectPoint, + srcMacAddress, dstConnectPoint); + submitReactiveIntent(srcIpPrefix, dstToSrcIntent); + } + } + + /** + * Generates MultiPointToSinglePointIntent for both source host and + * destination host located in local SDN network. + * + * @param dstIpAddress the destination IP address + * @param dstConnectPoint the destination host connect point + * @param dstMacAddress the MAC address of destination host + * @param srcConnectPoint the connect point where packet-in from + * @return the generated MultiPointToSinglePointIntent + */ + private MultiPointToSinglePointIntent hostToHostIntentGenerator( + IpAddress dstIpAddress, + ConnectPoint dstConnectPoint, + MacAddress dstMacAddress, + ConnectPoint srcConnectPoint) { + checkNotNull(dstIpAddress); + checkNotNull(dstConnectPoint); + checkNotNull(dstMacAddress); + checkNotNull(srcConnectPoint); + + Set<ConnectPoint> ingressPoints = new HashSet<ConnectPoint>(); + ingressPoints.add(srcConnectPoint); + IpPrefix dstIpPrefix = dstIpAddress.toIpPrefix(); + + TrafficSelector.Builder selector = DefaultTrafficSelector.builder(); + if (dstIpAddress.isIp4()) { + selector.matchEthType(Ethernet.TYPE_IPV4); + selector.matchIPDst(dstIpPrefix); + } else { + selector.matchEthType(Ethernet.TYPE_IPV6); + selector.matchIPv6Dst(dstIpPrefix); + } + + // Rewrite the destination MAC address + TrafficTreatment.Builder treatment = + DefaultTrafficTreatment.builder().setEthDst(dstMacAddress); + + Key key = Key.of(dstIpPrefix.toString(), appId); + int priority = dstIpPrefix.prefixLength() * PRIORITY_MULTIPLIER + + PRIORITY_OFFSET; + MultiPointToSinglePointIntent intent = + MultiPointToSinglePointIntent.builder() + .appId(appId) + .key(key) + .selector(selector.build()) + .treatment(treatment.build()) + .ingressPoints(ingressPoints) + .egressPoint(dstConnectPoint) + .priority(priority) + .constraints(CONSTRAINTS) + .build(); + + log.trace("Generates ConnectivityHostToHost = {} ", intent); + return intent; + } + + @Override + public void updateExistingMp2pIntent(IpPrefix ipPrefix, + ConnectPoint ingressConnectPoint) { + checkNotNull(ipPrefix); + checkNotNull(ingressConnectPoint); + + MultiPointToSinglePointIntent existingIntent = + getExistingMp2pIntent(ipPrefix); + if (existingIntent != null) { + Set<ConnectPoint> ingressPoints = existingIntent.ingressPoints(); + // Add host connect point into ingressPoints of the existing intent + if (ingressPoints.add(ingressConnectPoint)) { + MultiPointToSinglePointIntent updatedMp2pIntent = + MultiPointToSinglePointIntent.builder() + .appId(appId) + .key(existingIntent.key()) + .selector(existingIntent.selector()) + .treatment(existingIntent.treatment()) + .ingressPoints(ingressPoints) + .egressPoint(existingIntent.egressPoint()) + .priority(existingIntent.priority()) + .constraints(CONSTRAINTS) + .build(); + + log.trace("Update an existing MultiPointToSinglePointIntent " + + "to new intent = {} ", updatedMp2pIntent); + submitReactiveIntent(ipPrefix, updatedMp2pIntent); + } + // If adding ingressConnectPoint to ingressPoints failed, it + // because between the time interval from checking existing intent + // to generating new intent, onos updated this intent due to other + // packet-in and the new intent also includes the + // ingressConnectPoint. This will not affect reactive routing. + } + } + + @Override + public boolean mp2pIntentExists(IpPrefix ipPrefix) { + checkNotNull(ipPrefix); + return routeIntents.get(ipPrefix) != null; + } + + /** + * Gets the existing MultiPointToSinglePointIntent from memory for a given + * IP prefix. + * + * @param ipPrefix the IP prefix used to find MultiPointToSinglePointIntent + * @return the MultiPointToSinglePointIntent if found, otherwise null + */ + private MultiPointToSinglePointIntent getExistingMp2pIntent(IpPrefix + ipPrefix) { + checkNotNull(ipPrefix); + return routeIntents.get(ipPrefix); + } +} diff --git a/framework/src/onos/apps/sdnip/src/main/java/org/onosproject/sdnip/PeerConnectivityManager.java b/framework/src/onos/apps/sdnip/src/main/java/org/onosproject/sdnip/PeerConnectivityManager.java new file mode 100644 index 00000000..459db2b7 --- /dev/null +++ b/framework/src/onos/apps/sdnip/src/main/java/org/onosproject/sdnip/PeerConnectivityManager.java @@ -0,0 +1,319 @@ +/* + * Copyright 2014-2015 Open Networking Laboratory + * + * Licensed 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.onosproject.sdnip; + +import org.onlab.packet.Ethernet; +import org.onlab.packet.IPv4; +import org.onlab.packet.IPv6; +import org.onlab.packet.IpAddress; +import org.onlab.packet.IpPrefix; +import org.onlab.packet.TpPort; +import org.onosproject.core.ApplicationId; +import org.onosproject.net.config.NetworkConfigService; +import org.onosproject.incubator.net.intf.Interface; +import org.onosproject.incubator.net.intf.InterfaceService; +import org.onosproject.net.ConnectPoint; +import org.onosproject.net.flow.DefaultTrafficSelector; +import org.onosproject.net.flow.DefaultTrafficTreatment; +import org.onosproject.net.flow.TrafficSelector; +import org.onosproject.net.flow.TrafficTreatment; +import org.onosproject.net.host.InterfaceIpAddress; +import org.onosproject.net.intent.PointToPointIntent; +import org.onosproject.routing.RoutingService; +import org.onosproject.routing.config.BgpConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Manages the connectivity requirements between peers. + */ +public class PeerConnectivityManager { + private static final int PRIORITY_OFFSET = 1000; + + private static final Logger log = LoggerFactory.getLogger( + PeerConnectivityManager.class); + + private static final short BGP_PORT = 179; + + private final IntentSynchronizer intentSynchronizer; + private final NetworkConfigService configService; + private final InterfaceService interfaceService; + + private final ApplicationId appId; + private final ApplicationId routerAppId; + + /** + * Creates a new PeerConnectivityManager. + * + * @param appId the application ID + * @param intentSynchronizer the intent synchronizer + * @param configService the SDN-IP config service + * @param interfaceService the interface service + * @param routerAppId application ID + */ + public PeerConnectivityManager(ApplicationId appId, + IntentSynchronizer intentSynchronizer, + NetworkConfigService configService, + ApplicationId routerAppId, + InterfaceService interfaceService) { + this.appId = appId; + this.intentSynchronizer = intentSynchronizer; + this.configService = configService; + this.routerAppId = routerAppId; + this.interfaceService = interfaceService; + } + + /** + * Starts the peer connectivity manager. + */ + public void start() { + setUpConnectivity(); + } + + /** + * Stops the peer connectivity manager. + */ + public void stop() { + } + + /** + * Sets up paths to establish connectivity between all internal + * BGP speakers and external BGP peers. + */ + private void setUpConnectivity() { + List<PointToPointIntent> intents = new ArrayList<>(); + + BgpConfig config = configService.getConfig(routerAppId, RoutingService.CONFIG_CLASS); + + if (config == null) { + log.warn("No BgpConfig found"); + return; + } + + for (BgpConfig.BgpSpeakerConfig bgpSpeaker : config.bgpSpeakers()) { + log.debug("Start to set up BGP paths for BGP speaker: {}", + bgpSpeaker); + + intents.addAll(buildSpeakerIntents(bgpSpeaker)); + } + + // Submit all the intents. + intentSynchronizer.submitPeerIntents(intents); + } + + private Collection<PointToPointIntent> buildSpeakerIntents(BgpConfig.BgpSpeakerConfig speaker) { + List<PointToPointIntent> intents = new ArrayList<>(); + + for (IpAddress peerAddress : speaker.peers()) { + Interface peeringInterface = interfaceService.getMatchingInterface(peerAddress); + + if (peeringInterface == null) { + log.debug("No peering interface found for peer {} on speaker {}", + peerAddress, speaker); + continue; + } + + IpAddress peeringAddress = null; + for (InterfaceIpAddress address : peeringInterface.ipAddresses()) { + if (address.subnetAddress().contains(peerAddress)) { + peeringAddress = address.ipAddress(); + break; + } + } + + checkNotNull(peeringAddress); + + intents.addAll(buildIntents(speaker.connectPoint(), peeringAddress, + peeringInterface.connectPoint(), peerAddress)); + } + + return intents; + } + + /** + * Builds the required intents between the two pairs of connect points and + * IP addresses. + * + * @param portOne the first connect point + * @param ipOne the first IP address + * @param portTwo the second connect point + * @param ipTwo the second IP address + * @return the intents to install + */ + private Collection<PointToPointIntent> buildIntents(ConnectPoint portOne, + IpAddress ipOne, + ConnectPoint portTwo, + IpAddress ipTwo) { + + List<PointToPointIntent> intents = new ArrayList<>(); + + TrafficTreatment treatment = DefaultTrafficTreatment.emptyTreatment(); + + TrafficSelector selector; + + byte tcpProtocol; + byte icmpProtocol; + + if (ipOne.isIp4()) { + tcpProtocol = IPv4.PROTOCOL_TCP; + icmpProtocol = IPv4.PROTOCOL_ICMP; + } else { + tcpProtocol = IPv6.PROTOCOL_TCP; + icmpProtocol = IPv6.PROTOCOL_ICMP6; + } + + // Path from BGP speaker to BGP peer matching destination TCP port 179 + selector = buildSelector(tcpProtocol, + ipOne, + ipTwo, + null, + BGP_PORT); + + intents.add(PointToPointIntent.builder() + .appId(appId) + .selector(selector) + .treatment(treatment) + .ingressPoint(portOne) + .egressPoint(portTwo) + .priority(PRIORITY_OFFSET) + .build()); + + // Path from BGP speaker to BGP peer matching source TCP port 179 + selector = buildSelector(tcpProtocol, + ipOne, + ipTwo, + BGP_PORT, + null); + + intents.add(PointToPointIntent.builder() + .appId(appId) + .selector(selector) + .treatment(treatment) + .ingressPoint(portOne) + .egressPoint(portTwo) + .priority(PRIORITY_OFFSET) + .build()); + + // Path from BGP peer to BGP speaker matching destination TCP port 179 + selector = buildSelector(tcpProtocol, + ipTwo, + ipOne, + null, + BGP_PORT); + + intents.add(PointToPointIntent.builder() + .appId(appId) + .selector(selector) + .treatment(treatment) + .ingressPoint(portTwo) + .egressPoint(portOne) + .priority(PRIORITY_OFFSET) + .build()); + + // Path from BGP peer to BGP speaker matching source TCP port 179 + selector = buildSelector(tcpProtocol, + ipTwo, + ipOne, + BGP_PORT, + null); + + intents.add(PointToPointIntent.builder() + .appId(appId) + .selector(selector) + .treatment(treatment) + .ingressPoint(portTwo) + .egressPoint(portOne) + .priority(PRIORITY_OFFSET) + .build()); + + // ICMP path from BGP speaker to BGP peer + selector = buildSelector(icmpProtocol, + ipOne, + ipTwo, + null, + null); + + intents.add(PointToPointIntent.builder() + .appId(appId) + .selector(selector) + .treatment(treatment) + .ingressPoint(portOne) + .egressPoint(portTwo) + .priority(PRIORITY_OFFSET) + .build()); + + // ICMP path from BGP peer to BGP speaker + selector = buildSelector(icmpProtocol, + ipTwo, + ipOne, + null, + null); + + intents.add(PointToPointIntent.builder() + .appId(appId) + .selector(selector) + .treatment(treatment) + .ingressPoint(portTwo) + .egressPoint(portOne) + .priority(PRIORITY_OFFSET) + .build()); + + return intents; + } + + /** + * Builds a traffic selector based on the set of input parameters. + * + * @param ipProto IP protocol + * @param srcIp source IP address + * @param dstIp destination IP address + * @param srcTcpPort source TCP port, or null if shouldn't be set + * @param dstTcpPort destination TCP port, or null if shouldn't be set + * @return the new traffic selector + */ + private TrafficSelector buildSelector(byte ipProto, IpAddress srcIp, + IpAddress dstIp, Short srcTcpPort, + Short dstTcpPort) { + TrafficSelector.Builder builder = DefaultTrafficSelector.builder() + .matchEthType(Ethernet.TYPE_IPV4) + .matchIPProtocol(ipProto); + + if (dstIp.isIp4()) { + builder.matchIPSrc(IpPrefix.valueOf(srcIp, IpPrefix.MAX_INET_MASK_LENGTH)) + .matchIPDst(IpPrefix.valueOf(dstIp, IpPrefix.MAX_INET_MASK_LENGTH)); + } else { + builder.matchIPv6Src(IpPrefix.valueOf(srcIp, IpPrefix.MAX_INET6_MASK_LENGTH)) + .matchIPv6Dst(IpPrefix.valueOf(dstIp, IpPrefix.MAX_INET6_MASK_LENGTH)); + } + + if (srcTcpPort != null) { + builder.matchTcpSrc(TpPort.tpPort(srcTcpPort)); + } + + if (dstTcpPort != null) { + builder.matchTcpDst(TpPort.tpPort(dstTcpPort)); + } + + return builder.build(); + } + +} diff --git a/framework/src/onos/apps/sdnip/src/main/java/org/onosproject/sdnip/SdnIp.java b/framework/src/onos/apps/sdnip/src/main/java/org/onosproject/sdnip/SdnIp.java new file mode 100644 index 00000000..3d1fe65c --- /dev/null +++ b/framework/src/onos/apps/sdnip/src/main/java/org/onosproject/sdnip/SdnIp.java @@ -0,0 +1,179 @@ +/* + * Copyright 2014-2015 Open Networking Laboratory + * + * Licensed 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.onosproject.sdnip; + +import org.apache.felix.scr.annotations.Activate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Deactivate; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.ReferenceCardinality; +import org.apache.felix.scr.annotations.Service; +import org.onosproject.cluster.ClusterService; +import org.onosproject.cluster.ControllerNode; +import org.onosproject.cluster.LeadershipEvent; +import org.onosproject.cluster.LeadershipEventListener; +import org.onosproject.cluster.LeadershipService; +import org.onosproject.core.ApplicationId; +import org.onosproject.core.CoreService; +import org.onosproject.net.config.NetworkConfigService; +import org.onosproject.incubator.net.intf.InterfaceService; +import org.onosproject.net.host.HostService; +import org.onosproject.net.intent.IntentService; +import org.onosproject.routing.RoutingService; +import org.onosproject.routing.config.RoutingConfigurationService; +import org.slf4j.Logger; + +import java.util.Objects; + +import static org.slf4j.LoggerFactory.getLogger; + +/** + * Component for the SDN-IP peering application. + */ +@Component(immediate = true) +@Service +public class SdnIp implements SdnIpService { + + private static final String SDN_IP_APP = "org.onosproject.sdnip"; + private final Logger log = getLogger(getClass()); + + @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY) + protected CoreService coreService; + + @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY) + protected IntentService intentService; + + @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY) + protected HostService hostService; + + @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY) + protected ClusterService clusterService; + + @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY) + protected LeadershipService leadershipService; + + @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY) + protected RoutingService routingService; + + @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY) + protected RoutingConfigurationService config; + + @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY) + protected NetworkConfigService networkConfigService; + + @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY) + protected InterfaceService interfaceService; + + private IntentSynchronizer intentSynchronizer; + private PeerConnectivityManager peerConnectivity; + + private LeadershipEventListener leadershipEventListener = + new InnerLeadershipEventListener(); + private ApplicationId appId; + private ControllerNode localControllerNode; + + @Activate + protected void activate() { + log.info("SDN-IP started"); + + appId = coreService.registerApplication(SDN_IP_APP); + + localControllerNode = clusterService.getLocalNode(); + + intentSynchronizer = new IntentSynchronizer(appId, intentService, + hostService, + config, + interfaceService); + intentSynchronizer.start(); + + peerConnectivity = new PeerConnectivityManager(appId, + intentSynchronizer, + networkConfigService, + coreService.getAppId(RoutingService.ROUTER_APP_ID), + interfaceService); + peerConnectivity.start(); + + routingService.addFibListener(intentSynchronizer); + routingService.addIntentRequestListener(intentSynchronizer); + routingService.start(); + + leadershipService.addListener(leadershipEventListener); + leadershipService.runForLeadership(appId.name()); + } + + @Deactivate + protected void deactivate() { + routingService.stop(); + peerConnectivity.stop(); + intentSynchronizer.stop(); + + leadershipService.withdraw(appId.name()); + leadershipService.removeListener(leadershipEventListener); + + log.info("SDN-IP Stopped"); + } + + @Override + public void modifyPrimary(boolean isPrimary) { + intentSynchronizer.leaderChanged(isPrimary); + } + + /** + * Converts DPIDs of the form xx:xx:xx:xx:xx:xx:xx to OpenFlow provider + * device URIs. + * + * @param dpid the DPID string to convert + * @return the URI string for this device + */ + static String dpidToUri(String dpid) { + return "of:" + dpid.replace(":", ""); + } + + /** + * A listener for Leadership Events. + */ + private class InnerLeadershipEventListener + implements LeadershipEventListener { + + @Override + public void event(LeadershipEvent event) { + log.debug("Leadership Event: time = {} type = {} event = {}", + event.time(), event.type(), event); + + if (!event.subject().topic().equals(appId.name())) { + return; // Not our topic: ignore + } + if (!Objects.equals(event.subject().leader(), localControllerNode.id())) { + return; // The event is not about this instance: ignore + } + + switch (event.type()) { + case LEADER_ELECTED: + log.info("SDN-IP Leader Elected"); + intentSynchronizer.leaderChanged(true); + break; + case LEADER_BOOTED: + log.info("SDN-IP Leader Lost Election"); + intentSynchronizer.leaderChanged(false); + break; + case LEADER_REELECTED: + break; + default: + break; + } + } + } +} diff --git a/framework/src/onos/apps/sdnip/src/main/java/org/onosproject/sdnip/SdnIpService.java b/framework/src/onos/apps/sdnip/src/main/java/org/onosproject/sdnip/SdnIpService.java new file mode 100644 index 00000000..d26d3060 --- /dev/null +++ b/framework/src/onos/apps/sdnip/src/main/java/org/onosproject/sdnip/SdnIpService.java @@ -0,0 +1,31 @@ +/* + * Copyright 2014-2015 Open Networking Laboratory + * + * Licensed 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.onosproject.sdnip; + +/** + * Service interface exported by SDN-IP. + */ +public interface SdnIpService { + + /** + * Changes whether this SDN-IP instance is the primary or not based on the + * boolean parameter. + * + * @param isPrimary true if the instance is primary, false if it is not + */ + void modifyPrimary(boolean isPrimary); + +} diff --git a/framework/src/onos/apps/sdnip/src/main/java/org/onosproject/sdnip/cli/PrimaryChangeCommand.java b/framework/src/onos/apps/sdnip/src/main/java/org/onosproject/sdnip/cli/PrimaryChangeCommand.java new file mode 100644 index 00000000..72cc112e --- /dev/null +++ b/framework/src/onos/apps/sdnip/src/main/java/org/onosproject/sdnip/cli/PrimaryChangeCommand.java @@ -0,0 +1,40 @@ +/* + * Copyright 2014 Open Networking Laboratory + * + * Licensed 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.onosproject.sdnip.cli; + +import org.apache.karaf.shell.commands.Argument; +import org.apache.karaf.shell.commands.Command; +import org.onosproject.cli.AbstractShellCommand; +import org.onosproject.sdnip.SdnIpService; + +/** + * Command to change whether this SDNIP instance is primary or not. + */ +@Command(scope = "onos", name = "sdnip-set-primary", + description = "Changes the primary status of this SDN-IP instance") +public class PrimaryChangeCommand extends AbstractShellCommand { + + @Argument(index = 0, name = "isPrimary", + description = "True if this instance should be primary, false if not", + required = true, multiValued = false) + boolean isPrimary = false; + + @Override + protected void execute() { + get(SdnIpService.class).modifyPrimary(isPrimary); + } + +} diff --git a/framework/src/onos/apps/sdnip/src/main/java/org/onosproject/sdnip/cli/package-info.java b/framework/src/onos/apps/sdnip/src/main/java/org/onosproject/sdnip/cli/package-info.java new file mode 100644 index 00000000..73ea2a4f --- /dev/null +++ b/framework/src/onos/apps/sdnip/src/main/java/org/onosproject/sdnip/cli/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2014 Open Networking Laboratory + * + * Licensed 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. + */ + +/** + * SDN-IP command-line handlers. + */ +package org.onosproject.sdnip.cli; diff --git a/framework/src/onos/apps/sdnip/src/main/java/org/onosproject/sdnip/package-info.java b/framework/src/onos/apps/sdnip/src/main/java/org/onosproject/sdnip/package-info.java new file mode 100644 index 00000000..1e7d8cb9 --- /dev/null +++ b/framework/src/onos/apps/sdnip/src/main/java/org/onosproject/sdnip/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2014 Open Networking Laboratory + * + * Licensed 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. + */ + +/** + * SDN-IP peering application. + */ +package org.onosproject.sdnip; diff --git a/framework/src/onos/apps/sdnip/src/main/resources/OSGI-INF/blueprint/shell-config.xml b/framework/src/onos/apps/sdnip/src/main/resources/OSGI-INF/blueprint/shell-config.xml new file mode 100644 index 00000000..3be1c79a --- /dev/null +++ b/framework/src/onos/apps/sdnip/src/main/resources/OSGI-INF/blueprint/shell-config.xml @@ -0,0 +1,23 @@ +<!-- + ~ Copyright 2014 Open Networking Laboratory + ~ + ~ Licensed 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. + --> +<blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0"> + + <command-bundle xmlns="http://karaf.apache.org/xmlns/shell/v1.1.0"> + <command> + <action class="org.onosproject.sdnip.cli.PrimaryChangeCommand"/> + </command> + </command-bundle> +</blueprint> diff --git a/framework/src/onos/apps/sdnip/src/main/resources/config-examples/README b/framework/src/onos/apps/sdnip/src/main/resources/config-examples/README new file mode 100644 index 00000000..7642a4dd --- /dev/null +++ b/framework/src/onos/apps/sdnip/src/main/resources/config-examples/README @@ -0,0 +1,5 @@ +The SDN-IP configuration files should be copied to directory + $ONOS_HOME/tools/package/config + +After deployment and starting up the ONOS cluster, ONOS looks for these +configuration files in /opt/onos/config on each cluster member. diff --git a/framework/src/onos/apps/sdnip/src/main/resources/config-examples/sdnip.json b/framework/src/onos/apps/sdnip/src/main/resources/config-examples/sdnip.json new file mode 100644 index 00000000..c51de68a --- /dev/null +++ b/framework/src/onos/apps/sdnip/src/main/resources/config-examples/sdnip.json @@ -0,0 +1,86 @@ +{ + "bgpPeers" : [ + { + "attachmentDpid" : "00:00:00:00:00:00:00:a3", + "attachmentPort" : "1", + "ipAddress" : "192.168.10.1" + }, + { + "attachmentDpid" : "00:00:00:00:00:00:00:a5", + "attachmentPort" : "1", + "ipAddress" : "192.168.20.1" + }, + { + "attachmentDpid" : "00:00:00:00:00:00:00:a2", + "attachmentPort" : "1", + "ipAddress" : "192.168.30.1" + }, + { + "attachmentDpid" : "00:00:00:00:00:00:00:a6", + "attachmentPort" : "1", + "ipAddress" : "192.168.40.1" + }, + { + "attachmentDpid" : "00:00:00:00:00:00:00:a4", + "attachmentPort" : "4", + "ipAddress" : "192.168.60.1" + } + ], + "bgpSpeakers" : [ + { + "name" : "bgpSpeaker1", + "attachmentDpid" : "00:00:00:00:00:00:00:a1", + "attachmentPort" : "1", + "macAddress" : "00:00:00:00:00:01", + "interfaceAddresses" : [ + { + "interfaceDpid" : "00:00:00:00:00:00:00:a3", + "interfacePort" : "1", + "ipAddress" : "192.168.10.101" + }, + { + "interfaceDpid" : "00:00:00:00:00:00:00:a5", + "interfacePort" : "1", + "ipAddress" : "192.168.20.101" + }, + { + "interfaceDpid" : "00:00:00:00:00:00:00:a2", + "interfacePort" : "1", + "ipAddress" : "192.168.30.101" + }, + { + "interfaceDpid" : "00:00:00:00:00:00:00:a6", + "interfacePort" : "1", + "ipAddress" : "192.168.40.101" + }, + { + "interfaceDpid" : "00:00:00:00:00:00:00:a4", + "interfacePort" : "4", + "ipAddress" : "192.168.60.101" + } + + ] + + } + ], + "ip4LocalPrefixes" : [ + { + "ipPrefix" : "100.0.0.0/24", + "type" : "PUBLIC", + "gatewayIp" : "100.0.0.1" + }, + { + "ipPrefix" : "200.0.0.0/8", + "type" : "PUBLIC", + "gatewayIp" : "200.0.0.3" + }, + { + "ipPrefix" : "192.0.0.0/24", + "type" : "PRIVATE", + "gatewayIp" : "192.0.0.254" + } + ], + "ip6LocalPrefixes" : [ + ], + "virtualGatewayMacAddress" : "00:00:00:00:00:01" +} diff --git a/framework/src/onos/apps/sdnip/src/test/java/org/onosproject/sdnip/IntentSyncTest.java b/framework/src/onos/apps/sdnip/src/test/java/org/onosproject/sdnip/IntentSyncTest.java new file mode 100644 index 00000000..fc5782e4 --- /dev/null +++ b/framework/src/onos/apps/sdnip/src/test/java/org/onosproject/sdnip/IntentSyncTest.java @@ -0,0 +1,662 @@ +/* + * Copyright 2014-2015 Open Networking Laboratory + * + * Licensed 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.onosproject.sdnip; + +import com.google.common.collect.Sets; +import org.junit.Before; +import org.junit.Test; +import org.onlab.junit.TestUtils; +import org.onlab.junit.TestUtils.TestUtilsException; +import org.onlab.packet.Ethernet; +import org.onlab.packet.Ip4Address; +import org.onlab.packet.Ip4Prefix; +import org.onlab.packet.IpAddress; +import org.onlab.packet.IpPrefix; +import org.onlab.packet.MacAddress; +import org.onlab.packet.VlanId; +import org.onosproject.core.ApplicationId; +import org.onosproject.net.config.NetworkConfigService; +import org.onosproject.incubator.net.intf.Interface; +import org.onosproject.incubator.net.intf.InterfaceService; +import org.onosproject.net.ConnectPoint; +import org.onosproject.net.DeviceId; +import org.onosproject.net.PortNumber; +import org.onosproject.net.flow.DefaultTrafficSelector; +import org.onosproject.net.flow.DefaultTrafficTreatment; +import org.onosproject.net.flow.TrafficSelector; +import org.onosproject.net.flow.TrafficTreatment; +import org.onosproject.net.host.InterfaceIpAddress; +import org.onosproject.net.intent.AbstractIntentTest; +import org.onosproject.net.intent.Intent; +import org.onosproject.net.intent.IntentService; +import org.onosproject.net.intent.IntentState; +import org.onosproject.net.intent.MultiPointToSinglePointIntent; +import org.onosproject.routing.FibEntry; +import org.onosproject.routing.FibUpdate; +import org.onosproject.routing.RouteEntry; +import org.onosproject.routing.config.BgpPeer; +import org.onosproject.routing.config.RoutingConfigurationService; +import org.onosproject.sdnip.IntentSynchronizer.IntentKey; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.reset; +import static org.easymock.EasyMock.verify; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.onosproject.sdnip.TestIntentServiceHelper.eqExceptId; + +/** + * This class tests the intent synchronization function in the + * IntentSynchronizer class. + */ +public class IntentSyncTest extends AbstractIntentTest { + + private RoutingConfigurationService routingConfig; + private InterfaceService interfaceService; + private IntentService intentService; + private NetworkConfigService configService; + + private static final ConnectPoint SW1_ETH1 = new ConnectPoint( + DeviceId.deviceId("of:0000000000000001"), + PortNumber.portNumber(1)); + + private static final ConnectPoint SW2_ETH1 = new ConnectPoint( + DeviceId.deviceId("of:0000000000000002"), + PortNumber.portNumber(1)); + + private static final ConnectPoint SW3_ETH1 = new ConnectPoint( + DeviceId.deviceId("of:0000000000000003"), + PortNumber.portNumber(1)); + + private static final ConnectPoint SW4_ETH1 = new ConnectPoint( + DeviceId.deviceId("of:0000000000000004"), + PortNumber.portNumber(1)); + + private IntentSynchronizer intentSynchronizer; + private final Set<Interface> interfaces = Sets.newHashSet(); + + private static final ApplicationId APPID = new ApplicationId() { + @Override + public short id() { + return 1; + } + + @Override + public String name() { + return "SDNIP"; + } + }; + + @Before + public void setUp() throws Exception { + super.setUp(); + + routingConfig = createMock(RoutingConfigurationService.class); + interfaceService = createMock(InterfaceService.class); + configService = createMock(NetworkConfigService.class); + + // These will set expectations on routingConfig + setUpInterfaceService(); + setUpBgpPeers(); + + replay(routingConfig); + replay(interfaceService); + + intentService = createMock(IntentService.class); + + intentSynchronizer = new IntentSynchronizer(APPID, intentService, + null, routingConfig, + interfaceService); + } + + /** + * Sets up BGP peers in external networks. + */ + private void setUpBgpPeers() { + + Map<IpAddress, BgpPeer> peers = new HashMap<>(); + + String peerSw1Eth1 = "192.168.10.1"; + peers.put(IpAddress.valueOf(peerSw1Eth1), + new BgpPeer("00:00:00:00:00:00:00:01", 1, peerSw1Eth1)); + + // Two BGP peers are connected to switch 2 port 1. + String peer1Sw2Eth1 = "192.168.20.1"; + peers.put(IpAddress.valueOf(peer1Sw2Eth1), + new BgpPeer("00:00:00:00:00:00:00:02", 1, peer1Sw2Eth1)); + + String peer2Sw2Eth1 = "192.168.20.2"; + peers.put(IpAddress.valueOf(peer2Sw2Eth1), + new BgpPeer("00:00:00:00:00:00:00:02", 1, peer2Sw2Eth1)); + + String peer1Sw4Eth1 = "192.168.40.1"; + peers.put(IpAddress.valueOf(peer1Sw4Eth1), + new BgpPeer("00:00:00:00:00:00:00:04", 1, peer1Sw4Eth1)); + + expect(routingConfig.getBgpPeers()).andReturn(peers).anyTimes(); + } + + /** + * Sets up InterfaceService. + */ + private void setUpInterfaceService() { + Set<InterfaceIpAddress> interfaceIpAddresses1 = Sets.newHashSet(); + interfaceIpAddresses1.add(new InterfaceIpAddress( + IpAddress.valueOf("192.168.10.101"), + IpPrefix.valueOf("192.168.10.0/24"))); + Interface sw1Eth1 = new Interface(SW1_ETH1, + interfaceIpAddresses1, MacAddress.valueOf("00:00:00:00:00:01"), + VlanId.NONE); + interfaces.add(sw1Eth1); + + Set<InterfaceIpAddress> interfaceIpAddresses2 = Sets.newHashSet(); + interfaceIpAddresses2.add( + new InterfaceIpAddress(IpAddress.valueOf("192.168.20.101"), + IpPrefix.valueOf("192.168.20.0/24"))); + Interface sw2Eth1 = new Interface(SW2_ETH1, + interfaceIpAddresses2, MacAddress.valueOf("00:00:00:00:00:02"), + VlanId.NONE); + interfaces.add(sw2Eth1); + + Set<InterfaceIpAddress> interfaceIpAddresses3 = Sets.newHashSet(); + interfaceIpAddresses3.add( + new InterfaceIpAddress(IpAddress.valueOf("192.168.30.101"), + IpPrefix.valueOf("192.168.30.0/24"))); + Interface sw3Eth1 = new Interface(SW3_ETH1, + interfaceIpAddresses3, MacAddress.valueOf("00:00:00:00:00:03"), + VlanId.NONE); + interfaces.add(sw3Eth1); + + InterfaceIpAddress interfaceIpAddress4 = + new InterfaceIpAddress(IpAddress.valueOf("192.168.40.101"), + IpPrefix.valueOf("192.168.40.0/24")); + Interface sw4Eth1 = new Interface(SW4_ETH1, + Sets.newHashSet(interfaceIpAddress4), + MacAddress.valueOf("00:00:00:00:00:04"), + VlanId.vlanId((short) 1)); + + expect(interfaceService.getInterfacesByPort(SW4_ETH1)).andReturn( + Collections.singleton(sw4Eth1)).anyTimes(); + expect(interfaceService.getMatchingInterface(Ip4Address.valueOf("192.168.40.1"))) + .andReturn(sw4Eth1).anyTimes(); + + interfaces.add(sw4Eth1); + + expect(interfaceService.getInterfacesByPort(SW1_ETH1)).andReturn( + Collections.singleton(sw1Eth1)).anyTimes(); + expect(interfaceService.getMatchingInterface(Ip4Address.valueOf("192.168.10.1"))) + .andReturn(sw1Eth1).anyTimes(); + expect(interfaceService.getInterfacesByPort(SW2_ETH1)).andReturn( + Collections.singleton(sw2Eth1)).anyTimes(); + expect(interfaceService.getMatchingInterface(Ip4Address.valueOf("192.168.20.1"))) + .andReturn(sw2Eth1).anyTimes(); + expect(interfaceService.getInterfacesByPort(SW3_ETH1)).andReturn( + Collections.singleton(sw3Eth1)).anyTimes(); + expect(interfaceService.getMatchingInterface(Ip4Address.valueOf("192.168.30.1"))) + .andReturn(sw3Eth1).anyTimes(); + expect(interfaceService.getInterfaces()).andReturn(interfaces).anyTimes(); + } + + /** + * Tests adding a FIB entry to the IntentSynchronizer. + * + * We verify that the synchronizer records the correct state and that the + * correct intent is submitted to the IntentService. + * + * @throws TestUtilsException + */ + @Test + public void testFibAdd() throws TestUtilsException { + FibEntry fibEntry = new FibEntry( + Ip4Prefix.valueOf("1.1.1.0/24"), + Ip4Address.valueOf("192.168.10.1"), + MacAddress.valueOf("00:00:00:00:00:01")); + + // Construct a MultiPointToSinglePointIntent intent + TrafficSelector.Builder selectorBuilder = + DefaultTrafficSelector.builder(); + selectorBuilder.matchEthType(Ethernet.TYPE_IPV4).matchIPDst( + fibEntry.prefix()); + + TrafficTreatment.Builder treatmentBuilder = + DefaultTrafficTreatment.builder(); + treatmentBuilder.setEthDst(MacAddress.valueOf("00:00:00:00:00:01")); + + Set<ConnectPoint> ingressPoints = new HashSet<>(); + ingressPoints.add(SW2_ETH1); + ingressPoints.add(SW3_ETH1); + ingressPoints.add(SW4_ETH1); + + MultiPointToSinglePointIntent intent = + MultiPointToSinglePointIntent.builder() + .appId(APPID) + .selector(selectorBuilder.build()) + .treatment(treatmentBuilder.build()) + .ingressPoints(ingressPoints) + .egressPoint(SW1_ETH1) + .constraints(IntentSynchronizer.CONSTRAINTS) + .build(); + + // Setup the expected intents + intentService.submit(eqExceptId(intent)); + replay(intentService); + + intentSynchronizer.leaderChanged(true); + TestUtils.setField(intentSynchronizer, "isActivatedLeader", true); + + FibUpdate fibUpdate = new FibUpdate(FibUpdate.Type.UPDATE, + fibEntry); + intentSynchronizer.update(Collections.singleton(fibUpdate), + Collections.emptyList()); + + assertEquals(intentSynchronizer.getRouteIntents().size(), 1); + Intent firstIntent = + intentSynchronizer.getRouteIntents().iterator().next(); + IntentKey firstIntentKey = new IntentKey(firstIntent); + IntentKey intentKey = new IntentKey(intent); + assertTrue(firstIntentKey.equals(intentKey)); + verify(intentService); + } + + /** + * Tests adding a FIB entry with to a next hop in a VLAN. + * + * We verify that the synchronizer records the correct state and that the + * correct intent is submitted to the IntentService. + * + * @throws TestUtilsException + */ + @Test + public void testFibAddWithVlan() throws TestUtilsException { + FibEntry fibEntry = new FibEntry( + Ip4Prefix.valueOf("3.3.3.0/24"), + Ip4Address.valueOf("192.168.40.1"), + MacAddress.valueOf("00:00:00:00:00:04")); + + // Construct a MultiPointToSinglePointIntent intent + TrafficSelector.Builder selectorBuilder = + DefaultTrafficSelector.builder(); + selectorBuilder.matchEthType(Ethernet.TYPE_IPV4) + .matchIPDst(fibEntry.prefix()) + .matchVlanId(VlanId.ANY); + + TrafficTreatment.Builder treatmentBuilder = + DefaultTrafficTreatment.builder(); + treatmentBuilder.setEthDst(MacAddress.valueOf("00:00:00:00:00:04")) + .setVlanId(VlanId.vlanId((short) 1)); + + Set<ConnectPoint> ingressPoints = new HashSet<>(); + ingressPoints.add(SW1_ETH1); + ingressPoints.add(SW2_ETH1); + ingressPoints.add(SW3_ETH1); + + MultiPointToSinglePointIntent intent = + MultiPointToSinglePointIntent.builder() + .appId(APPID) + .selector(selectorBuilder.build()) + .treatment(treatmentBuilder.build()) + .ingressPoints(ingressPoints) + .egressPoint(SW4_ETH1) + .constraints(IntentSynchronizer.CONSTRAINTS) + .build(); + + // Setup the expected intents + intentService.submit(eqExceptId(intent)); + + replay(intentService); + + // Run the test + intentSynchronizer.leaderChanged(true); + TestUtils.setField(intentSynchronizer, "isActivatedLeader", true); + FibUpdate fibUpdate = new FibUpdate(FibUpdate.Type.UPDATE, fibEntry); + + intentSynchronizer.update(Collections.singleton(fibUpdate), + Collections.emptyList()); + + // Verify + assertEquals(intentSynchronizer.getRouteIntents().size(), 1); + Intent firstIntent = + intentSynchronizer.getRouteIntents().iterator().next(); + IntentKey firstIntentKey = new IntentKey(firstIntent); + IntentKey intentKey = new IntentKey(intent); + assertTrue(firstIntentKey.equals(intentKey)); + verify(intentService); + } + + /** + * Tests updating a FIB entry. + * + * We verify that the synchronizer records the correct state and that the + * correct intent is submitted to the IntentService. + * + * @throws TestUtilsException + */ + @Test + public void testFibUpdate() throws TestUtilsException { + // Firstly add a route + testFibAdd(); + + Intent addedIntent = + intentSynchronizer.getRouteIntents().iterator().next(); + + // Start to construct a new route entry and new intent + FibEntry fibEntryUpdate = new FibEntry( + Ip4Prefix.valueOf("1.1.1.0/24"), + Ip4Address.valueOf("192.168.20.1"), + MacAddress.valueOf("00:00:00:00:00:02")); + + // Construct a new MultiPointToSinglePointIntent intent + TrafficSelector.Builder selectorBuilderNew = + DefaultTrafficSelector.builder(); + selectorBuilderNew.matchEthType(Ethernet.TYPE_IPV4).matchIPDst( + fibEntryUpdate.prefix()); + + TrafficTreatment.Builder treatmentBuilderNew = + DefaultTrafficTreatment.builder(); + treatmentBuilderNew.setEthDst(MacAddress.valueOf("00:00:00:00:00:02")); + + + Set<ConnectPoint> ingressPointsNew = new HashSet<>(); + ingressPointsNew.add(SW1_ETH1); + ingressPointsNew.add(SW3_ETH1); + ingressPointsNew.add(SW4_ETH1); + + MultiPointToSinglePointIntent intentNew = + MultiPointToSinglePointIntent.builder() + .appId(APPID) + .selector(selectorBuilderNew.build()) + .treatment(treatmentBuilderNew.build()) + .ingressPoints(ingressPointsNew) + .egressPoint(SW2_ETH1) + .constraints(IntentSynchronizer.CONSTRAINTS) + .build(); + + // Set up test expectation + reset(intentService); + // Setup the expected intents + intentService.withdraw(eqExceptId(addedIntent)); + intentService.submit(eqExceptId(intentNew)); + replay(intentService); + + // Call the update() method in IntentSynchronizer class + intentSynchronizer.leaderChanged(true); + TestUtils.setField(intentSynchronizer, "isActivatedLeader", true); + FibUpdate fibUpdate = new FibUpdate(FibUpdate.Type.UPDATE, + fibEntryUpdate); + intentSynchronizer.update(Collections.singletonList(fibUpdate), + Collections.emptyList()); + + // Verify + assertEquals(intentSynchronizer.getRouteIntents().size(), 1); + Intent firstIntent = + intentSynchronizer.getRouteIntents().iterator().next(); + IntentKey firstIntentKey = new IntentKey(firstIntent); + IntentKey intentNewKey = new IntentKey(intentNew); + assertTrue(firstIntentKey.equals(intentNewKey)); + verify(intentService); + } + + /** + * Tests deleting a FIB entry. + * + * We verify that the synchronizer records the correct state and that the + * correct intent is withdrawn from the IntentService. + * + * @throws TestUtilsException + */ + @Test + public void testFibDelete() throws TestUtilsException { + // Firstly add a route + testFibAdd(); + + Intent addedIntent = + intentSynchronizer.getRouteIntents().iterator().next(); + + // Construct the existing route entry + FibEntry fibEntry = new FibEntry( + Ip4Prefix.valueOf("1.1.1.0/24"), null, null); + + // Set up expectation + reset(intentService); + // Setup the expected intents + intentService.withdraw(eqExceptId(addedIntent)); + replay(intentService); + + // Call the update() method in IntentSynchronizer class + intentSynchronizer.leaderChanged(true); + TestUtils.setField(intentSynchronizer, "isActivatedLeader", true); + FibUpdate fibUpdate = new FibUpdate(FibUpdate.Type.DELETE, fibEntry); + intentSynchronizer.update(Collections.emptyList(), + Collections.singletonList(fibUpdate)); + + // Verify + assertEquals(intentSynchronizer.getRouteIntents().size(), 0); + verify(intentService); + } + + /** + * This method tests the behavior of intent Synchronizer. + * + * @throws TestUtilsException + */ + @Test + public void testIntentSync() throws TestUtilsException { + + // + // Construct routes and intents. + // This test simulates the following cases during the master change + // time interval: + // 1. RouteEntry1 did not change and the intent also did not change. + // 2. RouteEntry2 was deleted, but the intent was not deleted. + // 3. RouteEntry3 was newly added, and the intent was also submitted. + // 4. RouteEntry4 was updated to RouteEntry4Update, and the intent was + // also updated to a new one. + // 5. RouteEntry5 did not change, but its intent id changed. + // 6. RouteEntry6 was newly added, but the intent was not submitted. + // + RouteEntry routeEntry1 = new RouteEntry( + Ip4Prefix.valueOf("1.1.1.0/24"), + Ip4Address.valueOf("192.168.10.1")); + + RouteEntry routeEntry2 = new RouteEntry( + Ip4Prefix.valueOf("2.2.2.0/24"), + Ip4Address.valueOf("192.168.20.1")); + + RouteEntry routeEntry3 = new RouteEntry( + Ip4Prefix.valueOf("3.3.3.0/24"), + Ip4Address.valueOf("192.168.30.1")); + + RouteEntry routeEntry4 = new RouteEntry( + Ip4Prefix.valueOf("4.4.4.0/24"), + Ip4Address.valueOf("192.168.30.1")); + + RouteEntry routeEntry4Update = new RouteEntry( + Ip4Prefix.valueOf("4.4.4.0/24"), + Ip4Address.valueOf("192.168.20.1")); + + RouteEntry routeEntry5 = new RouteEntry( + Ip4Prefix.valueOf("5.5.5.0/24"), + Ip4Address.valueOf("192.168.10.1")); + + RouteEntry routeEntry6 = new RouteEntry( + Ip4Prefix.valueOf("6.6.6.0/24"), + Ip4Address.valueOf("192.168.10.1")); + + RouteEntry routeEntry7 = new RouteEntry( + Ip4Prefix.valueOf("7.7.7.0/24"), + Ip4Address.valueOf("192.168.10.1")); + + MultiPointToSinglePointIntent intent1 = intentBuilder( + routeEntry1.prefix(), "00:00:00:00:00:01", SW1_ETH1); + MultiPointToSinglePointIntent intent2 = intentBuilder( + routeEntry2.prefix(), "00:00:00:00:00:02", SW2_ETH1); + MultiPointToSinglePointIntent intent3 = intentBuilder( + routeEntry3.prefix(), "00:00:00:00:00:03", SW3_ETH1); + MultiPointToSinglePointIntent intent4 = intentBuilder( + routeEntry4.prefix(), "00:00:00:00:00:03", SW3_ETH1); + MultiPointToSinglePointIntent intent4Update = intentBuilder( + routeEntry4Update.prefix(), "00:00:00:00:00:02", SW2_ETH1); + MultiPointToSinglePointIntent intent5 = intentBuilder( + routeEntry5.prefix(), "00:00:00:00:00:01", SW1_ETH1); + MultiPointToSinglePointIntent intent7 = intentBuilder( + routeEntry7.prefix(), "00:00:00:00:00:01", SW1_ETH1); + + // Compose a intent, which is equal to intent5 but the id is different. + MultiPointToSinglePointIntent intent5New = + staticIntentBuilder(intent5, routeEntry5, "00:00:00:00:00:01"); + assertThat(IntentSynchronizer.IntentKey.equalIntents( + intent5, intent5New), + is(true)); + assertFalse(intent5.equals(intent5New)); + + MultiPointToSinglePointIntent intent6 = intentBuilder( + routeEntry6.prefix(), "00:00:00:00:00:01", SW1_ETH1); + + // Set up the routeIntents field in IntentSynchronizer class + ConcurrentHashMap<IpPrefix, MultiPointToSinglePointIntent> + routeIntents = new ConcurrentHashMap<>(); + routeIntents.put(routeEntry1.prefix(), intent1); + routeIntents.put(routeEntry3.prefix(), intent3); + routeIntents.put(routeEntry4Update.prefix(), intent4Update); + routeIntents.put(routeEntry5.prefix(), intent5New); + routeIntents.put(routeEntry6.prefix(), intent6); + routeIntents.put(routeEntry7.prefix(), intent7); + TestUtils.setField(intentSynchronizer, "routeIntents", routeIntents); + + // Set up expectation + reset(intentService); + Set<Intent> intents = new HashSet<>(); + intents.add(intent1); + expect(intentService.getIntentState(intent1.key())) + .andReturn(IntentState.INSTALLED).anyTimes(); + intents.add(intent2); + expect(intentService.getIntentState(intent2.key())) + .andReturn(IntentState.INSTALLED).anyTimes(); + intents.add(intent4); + expect(intentService.getIntentState(intent4.key())) + .andReturn(IntentState.INSTALLED).anyTimes(); + intents.add(intent5); + expect(intentService.getIntentState(intent5.key())) + .andReturn(IntentState.INSTALLED).anyTimes(); + intents.add(intent7); + expect(intentService.getIntentState(intent7.key())) + .andReturn(IntentState.WITHDRAWING).anyTimes(); + expect(intentService.getIntents()).andReturn(intents).anyTimes(); + + intentService.withdraw(intent2); + intentService.withdraw(intent4); + + intentService.submit(intent3); + intentService.submit(intent4Update); + intentService.submit(intent6); + intentService.submit(intent7); + replay(intentService); + + // Start the test + intentSynchronizer.leaderChanged(true); + intentSynchronizer.synchronizeIntents(); + + // Verify + assertEquals(intentSynchronizer.getRouteIntents().size(), 6); + assertTrue(intentSynchronizer.getRouteIntents().contains(intent1)); + assertTrue(intentSynchronizer.getRouteIntents().contains(intent3)); + assertTrue(intentSynchronizer.getRouteIntents().contains(intent4Update)); + assertTrue(intentSynchronizer.getRouteIntents().contains(intent5)); + assertTrue(intentSynchronizer.getRouteIntents().contains(intent6)); + + verify(intentService); + } + + /** + * MultiPointToSinglePointIntent builder. + * + * @param ipPrefix the ipPrefix to match + * @param nextHopMacAddress to which the destination MAC address in packet + * should be rewritten + * @param egressPoint to which packets should be sent + * @return the constructed MultiPointToSinglePointIntent + */ + private MultiPointToSinglePointIntent intentBuilder(IpPrefix ipPrefix, + String nextHopMacAddress, ConnectPoint egressPoint) { + + TrafficSelector.Builder selectorBuilder = + DefaultTrafficSelector.builder(); + if (ipPrefix.isIp4()) { + selectorBuilder.matchEthType(Ethernet.TYPE_IPV4); // IPv4 + selectorBuilder.matchIPDst(ipPrefix); + } else { + selectorBuilder.matchEthType(Ethernet.TYPE_IPV6); // IPv6 + selectorBuilder.matchIPv6Dst(ipPrefix); + } + + TrafficTreatment.Builder treatmentBuilder = + DefaultTrafficTreatment.builder(); + treatmentBuilder.setEthDst(MacAddress.valueOf(nextHopMacAddress)); + + Set<ConnectPoint> ingressPoints = new HashSet<>(); + for (Interface intf : interfaces) { + if (!intf.connectPoint().equals(egressPoint)) { + ConnectPoint srcPort = intf.connectPoint(); + ingressPoints.add(srcPort); + } + } + MultiPointToSinglePointIntent intent = + MultiPointToSinglePointIntent.builder() + .appId(APPID) + .selector(selectorBuilder.build()) + .treatment(treatmentBuilder.build()) + .ingressPoints(ingressPoints) + .egressPoint(egressPoint) + .constraints(IntentSynchronizer.CONSTRAINTS) + .build(); + return intent; + } + + /** + * A static MultiPointToSinglePointIntent builder, the returned intent is + * equal to the input intent except that the id is different. + * + * @param intent the intent to be used for building a new intent + * @param routeEntry the relative routeEntry of the intent + * @return the newly constructed MultiPointToSinglePointIntent + * @throws TestUtilsException + */ + private MultiPointToSinglePointIntent staticIntentBuilder( + MultiPointToSinglePointIntent intent, RouteEntry routeEntry, + String nextHopMacAddress) throws TestUtilsException { + + // Use a different egress ConnectPoint with that in intent + // to generate a different id + MultiPointToSinglePointIntent intentNew = intentBuilder( + routeEntry.prefix(), nextHopMacAddress, SW2_ETH1); + TestUtils.setField(intentNew, "egressPoint", intent.egressPoint()); + TestUtils.setField(intentNew, + "ingressPoints", intent.ingressPoints()); + return intentNew; + } +} diff --git a/framework/src/onos/apps/sdnip/src/test/java/org/onosproject/sdnip/PeerConnectivityManagerTest.java b/framework/src/onos/apps/sdnip/src/test/java/org/onosproject/sdnip/PeerConnectivityManagerTest.java new file mode 100644 index 00000000..d89c3c2b --- /dev/null +++ b/framework/src/onos/apps/sdnip/src/test/java/org/onosproject/sdnip/PeerConnectivityManagerTest.java @@ -0,0 +1,565 @@ +/* + * Copyright 2014-2015 Open Networking Laboratory + * + * Licensed 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.onosproject.sdnip; + +import com.google.common.collect.Sets; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.onlab.junit.TestUtils; +import org.onlab.junit.TestUtils.TestUtilsException; +import org.onlab.packet.Ethernet; +import org.onlab.packet.IPv4; +import org.onlab.packet.IpAddress; +import org.onlab.packet.IpPrefix; +import org.onlab.packet.MacAddress; +import org.onlab.packet.TpPort; +import org.onlab.packet.VlanId; +import org.onosproject.core.ApplicationId; +import org.onosproject.net.config.NetworkConfigService; +import org.onosproject.incubator.net.intf.Interface; +import org.onosproject.incubator.net.intf.InterfaceService; +import org.onosproject.net.ConnectPoint; +import org.onosproject.net.DeviceId; +import org.onosproject.net.PortNumber; +import org.onosproject.net.flow.DefaultTrafficSelector; +import org.onosproject.net.flow.DefaultTrafficTreatment; +import org.onosproject.net.flow.TrafficSelector; +import org.onosproject.net.flow.TrafficTreatment; +import org.onosproject.net.host.InterfaceIpAddress; +import org.onosproject.net.intent.AbstractIntentTest; +import org.onosproject.net.intent.Intent; +import org.onosproject.net.intent.IntentService; +import org.onosproject.net.intent.PointToPointIntent; +import org.onosproject.routing.config.BgpConfig; +import org.onosproject.routing.config.BgpPeer; +import org.onosproject.routing.config.BgpSpeaker; +import org.onosproject.routing.config.InterfaceAddress; +import org.onosproject.routing.config.RoutingConfigurationService; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.reset; +import static org.easymock.EasyMock.verify; +import static org.onosproject.sdnip.TestIntentServiceHelper.eqExceptId; + +/** + * Unit tests for PeerConnectivityManager. + */ +public class PeerConnectivityManagerTest extends AbstractIntentTest { + + private static final ApplicationId APPID = new ApplicationId() { + @Override + public short id() { + return 0; + } + + @Override + public String name() { + return "foo"; + } + }; + + private static final ApplicationId CONFIG_APP_ID = APPID; + + private PeerConnectivityManager peerConnectivityManager; + private IntentSynchronizer intentSynchronizer; + private RoutingConfigurationService routingConfig; + private InterfaceService interfaceService; + private NetworkConfigService networkConfigService; + private IntentService intentService; + + private Set<BgpConfig.BgpSpeakerConfig> bgpSpeakers; + private Map<String, Interface> interfaces; + private Map<IpAddress, BgpPeer> peers; + + private BgpConfig bgpConfig; + + private Map<String, Interface> configuredInterfaces; + private Map<IpAddress, BgpPeer> configuredPeers; + private List<PointToPointIntent> intentList; + + private final String dpid1 = "00:00:00:00:00:00:00:01"; + private final String dpid2 = "00:00:00:00:00:00:00:02"; + + private final DeviceId deviceId1 = + DeviceId.deviceId(SdnIp.dpidToUri(dpid1)); + private final DeviceId deviceId2 = + DeviceId.deviceId(SdnIp.dpidToUri(dpid2)); + + // Interfaces connected to BGP speakers + private final ConnectPoint s1Eth100 = + new ConnectPoint(deviceId1, PortNumber.portNumber(100)); + private final ConnectPoint s2Eth100 = + new ConnectPoint(deviceId2, PortNumber.portNumber(100)); + + // Interfaces connected to BGP peers + private final ConnectPoint s1Eth1 = + new ConnectPoint(deviceId1, PortNumber.portNumber(1)); + private final ConnectPoint s2Eth1 = + new ConnectPoint(deviceId2, PortNumber.portNumber(1)); + + private final TrafficTreatment noTreatment = + DefaultTrafficTreatment.emptyTreatment(); + + @Before + public void setUp() throws Exception { + super.setUp(); + routingConfig = createMock(RoutingConfigurationService.class); + interfaceService = createMock(InterfaceService.class); + networkConfigService = createMock(NetworkConfigService.class); + bgpConfig = createMock(BgpConfig.class); + + // These will set expectations on routingConfig and interfaceService + bgpSpeakers = setUpBgpSpeakers(); + interfaces = Collections.unmodifiableMap(setUpInterfaces()); + peers = Collections.unmodifiableMap(setUpPeers()); + + initPeerConnectivity(); + intentList = setUpIntentList(); + } + + /** + * Sets up BGP speakers. + * + * @return configured BGP speakers as a map from speaker name to speaker + */ + private Set<BgpConfig.BgpSpeakerConfig> setUpBgpSpeakers() { + + BgpConfig.BgpSpeakerConfig speaker1 = new BgpConfig.BgpSpeakerConfig( + Optional.empty(), + s1Eth100, Collections.singleton(IpAddress.valueOf("192.168.10.1"))); + + BgpConfig.BgpSpeakerConfig speaker2 = new BgpConfig.BgpSpeakerConfig( + Optional.empty(), + s1Eth100, Sets.newHashSet(IpAddress.valueOf("192.168.20.1"), + IpAddress.valueOf("192.168.30.1"))); + + Set<BgpConfig.BgpSpeakerConfig> bgpSpeakers = Sets.newHashSet(); + bgpSpeakers.add(speaker1); + bgpSpeakers.add(speaker2); + + return bgpSpeakers; + } + + /** + * Sets up logical interfaces, which emulate the configured interfaces + * in SDN-IP application. + * + * @return configured interfaces as a MAP from Interface name to Interface + */ + private Map<String, Interface> setUpInterfaces() { + + configuredInterfaces = new HashMap<>(); + + String interfaceSw1Eth1 = "s1-eth1"; + InterfaceIpAddress ia1 = + new InterfaceIpAddress(IpAddress.valueOf("192.168.10.101"), + IpPrefix.valueOf("192.168.10.0/24")); + Interface intfsw1eth1 = new Interface(s1Eth1, + Collections.singleton(ia1), + MacAddress.valueOf("00:00:00:00:00:01"), + VlanId.NONE); + + configuredInterfaces.put(interfaceSw1Eth1, intfsw1eth1); + String interfaceSw2Eth1 = "s2-eth1"; + InterfaceIpAddress ia2 = + new InterfaceIpAddress(IpAddress.valueOf("192.168.20.101"), + IpPrefix.valueOf("192.168.20.0/24")); + Interface intfsw2eth1 = new Interface(s2Eth1, + Collections.singleton(ia2), + MacAddress.valueOf("00:00:00:00:00:02"), + VlanId.NONE); + configuredInterfaces.put(interfaceSw2Eth1, intfsw2eth1); + + String interfaceSw2Eth1intf2 = "s2-eth1_2"; + InterfaceIpAddress ia3 = + new InterfaceIpAddress(IpAddress.valueOf("192.168.30.101"), + IpPrefix.valueOf("192.168.30.0/24")); + Interface intfsw2eth1intf2 = new Interface(s2Eth1, + Collections.singleton(ia3), + MacAddress.valueOf("00:00:00:00:00:03"), + VlanId.NONE); + configuredInterfaces.put(interfaceSw2Eth1intf2, intfsw2eth1intf2); + + expect(interfaceService.getInterfacesByPort(s1Eth1)) + .andReturn(Collections.singleton(intfsw1eth1)).anyTimes(); + expect(interfaceService.getInterfacesByIp(IpAddress.valueOf("192.168.10.101"))) + .andReturn(Collections.singleton(intfsw1eth1)).anyTimes(); + expect(interfaceService.getMatchingInterface(IpAddress.valueOf("192.168.10.1"))) + .andReturn(intfsw1eth1).anyTimes(); + expect(interfaceService.getInterfacesByPort(s2Eth1)) + .andReturn(Collections.singleton(intfsw2eth1)).anyTimes(); + expect(interfaceService.getInterfacesByIp(IpAddress.valueOf("192.168.20.101"))) + .andReturn(Collections.singleton(intfsw2eth1)).anyTimes(); + expect(interfaceService.getMatchingInterface(IpAddress.valueOf("192.168.20.1"))) + .andReturn(intfsw2eth1).anyTimes(); + + expect(interfaceService.getInterfacesByIp(IpAddress.valueOf("192.168.30.101"))) + .andReturn(Collections.singleton(intfsw2eth1intf2)).anyTimes(); + expect(interfaceService.getMatchingInterface(IpAddress.valueOf("192.168.30.1"))) + .andReturn(intfsw2eth1intf2).anyTimes(); + + // Non-existent interface used during one of the tests + expect(interfaceService.getInterfacesByPort(new ConnectPoint( + DeviceId.deviceId(SdnIp.dpidToUri("00:00:00:00:00:00:01:00")), + PortNumber.portNumber(1)))) + .andReturn(null).anyTimes(); + + expect(interfaceService.getInterfaces()).andReturn( + Sets.newHashSet(configuredInterfaces.values())).anyTimes(); + + return configuredInterfaces; + } + + /** + * Sets up BGP daemon peers. + * + * @return configured BGP peers as a MAP from peer IP address to BgpPeer + */ + private Map<IpAddress, BgpPeer> setUpPeers() { + + configuredPeers = new HashMap<>(); + + String peerSw1Eth1 = "192.168.10.1"; + configuredPeers.put(IpAddress.valueOf(peerSw1Eth1), + new BgpPeer(dpid1, 1, peerSw1Eth1)); + + // Two BGP peers are connected to switch 2 port 1. + String peer1Sw2Eth1 = "192.168.20.1"; + configuredPeers.put(IpAddress.valueOf(peer1Sw2Eth1), + new BgpPeer(dpid2, 1, peer1Sw2Eth1)); + + String peer2Sw2Eth1 = "192.168.30.1"; + configuredPeers.put(IpAddress.valueOf(peer2Sw2Eth1), + new BgpPeer(dpid2, 1, peer2Sw2Eth1)); + + return configuredPeers; + } + + /** + * Sets up expected point to point intent list. + * + * @return point to point intent list + */ + private List<PointToPointIntent> setUpIntentList() { + + intentList = new ArrayList<>(); + + setUpBgpIntents(); + setUpIcmpIntents(); + + return intentList; + + } + + /** + * Constructs a BGP intent and put it into the intentList. + * <p/> + * The purpose of this method is too simplify the setUpBgpIntents() method, + * and to make the setUpBgpIntents() easy to read. + * + * @param srcPrefix source IP prefix to match + * @param dstPrefix destination IP prefix to match + * @param srcTcpPort source TCP port to match + * @param dstTcpPort destination TCP port to match + * @param srcConnectPoint source connect point for PointToPointIntent + * @param dstConnectPoint destination connect point for PointToPointIntent + */ + private void bgpPathintentConstructor(String srcPrefix, String dstPrefix, + Short srcTcpPort, Short dstTcpPort, + ConnectPoint srcConnectPoint, ConnectPoint dstConnectPoint) { + + TrafficSelector.Builder builder = DefaultTrafficSelector.builder() + .matchEthType(Ethernet.TYPE_IPV4) + .matchIPProtocol(IPv4.PROTOCOL_TCP) + .matchIPSrc(IpPrefix.valueOf(srcPrefix)) + .matchIPDst(IpPrefix.valueOf(dstPrefix)); + + if (srcTcpPort != null) { + builder.matchTcpSrc(TpPort.tpPort(srcTcpPort)); + } + if (dstTcpPort != null) { + builder.matchTcpDst(TpPort.tpPort(dstTcpPort)); + } + + PointToPointIntent intent = PointToPointIntent.builder() + .appId(APPID) + .selector(builder.build()) + .treatment(noTreatment) + .ingressPoint(srcConnectPoint) + .egressPoint(dstConnectPoint) + .build(); + + intentList.add(intent); + } + + /** + * Sets up intents for BGP paths. + */ + private void setUpBgpIntents() { + + Short bgpPort = 179; + + // Start to build intents between BGP speaker1 and BGP peer1 + bgpPathintentConstructor( + "192.168.10.101/32", "192.168.10.1/32", null, bgpPort, + s1Eth100, s1Eth1); + bgpPathintentConstructor( + "192.168.10.101/32", "192.168.10.1/32", bgpPort, null, + s1Eth100, s1Eth1); + + bgpPathintentConstructor( + "192.168.10.1/32", "192.168.10.101/32", null, bgpPort, + s1Eth1, s1Eth100); + bgpPathintentConstructor( + "192.168.10.1/32", "192.168.10.101/32", bgpPort, null, + s1Eth1, s1Eth100); + + // Start to build intents between BGP speaker1 and BGP peer2 + bgpPathintentConstructor( + "192.168.20.101/32", "192.168.20.1/32", null, bgpPort, + s1Eth100, s2Eth1); + bgpPathintentConstructor( + "192.168.20.101/32", "192.168.20.1/32", bgpPort, null, + s1Eth100, s2Eth1); + + bgpPathintentConstructor( + "192.168.20.1/32", "192.168.20.101/32", null, bgpPort, + s2Eth1, s1Eth100); + bgpPathintentConstructor( + "192.168.20.1/32", "192.168.20.101/32", bgpPort, null, + s2Eth1, s1Eth100); + + // + // Start to build intents between BGP speaker3 and BGP peer1 + bgpPathintentConstructor( + "192.168.30.101/32", "192.168.30.1/32", null, bgpPort, + s1Eth100, s2Eth1); + bgpPathintentConstructor( + "192.168.30.101/32", "192.168.30.1/32", bgpPort, null, + s1Eth100, s2Eth1); + + bgpPathintentConstructor( + "192.168.30.1/32", "192.168.30.101/32", null, bgpPort, + s2Eth1, s1Eth100); + bgpPathintentConstructor( + "192.168.30.1/32", "192.168.30.101/32", bgpPort, null, + s2Eth1, s1Eth100); + } + + /** + * Constructs a BGP intent and put it into the intentList. + * <p/> + * The purpose of this method is too simplify the setUpBgpIntents() method, + * and to make the setUpBgpIntents() easy to read. + * + * @param srcPrefix source IP prefix to match + * @param dstPrefix destination IP prefix to match + * @param srcConnectPoint source connect point for PointToPointIntent + * @param dstConnectPoint destination connect point for PointToPointIntent + */ + private void icmpPathintentConstructor(String srcPrefix, String dstPrefix, + ConnectPoint srcConnectPoint, ConnectPoint dstConnectPoint) { + + TrafficSelector selector = DefaultTrafficSelector.builder() + .matchEthType(Ethernet.TYPE_IPV4) + .matchIPProtocol(IPv4.PROTOCOL_ICMP) + .matchIPSrc(IpPrefix.valueOf(srcPrefix)) + .matchIPDst(IpPrefix.valueOf(dstPrefix)) + .build(); + + PointToPointIntent intent = PointToPointIntent.builder() + .appId(APPID) + .selector(selector) + .treatment(noTreatment) + .ingressPoint(srcConnectPoint) + .egressPoint(dstConnectPoint) + .build(); + + intentList.add(intent); + } + + /** + * Sets up intents for ICMP paths. + */ + private void setUpIcmpIntents() { + // Start to build intents between BGP speaker1 and BGP peer1 + icmpPathintentConstructor( + "192.168.10.101/32", "192.168.10.1/32", s1Eth100, s1Eth1); + icmpPathintentConstructor( + "192.168.10.1/32", "192.168.10.101/32", s1Eth1, s1Eth100); + + // Start to build intents between BGP speaker1 and BGP peer2 + icmpPathintentConstructor( + "192.168.20.101/32", "192.168.20.1/32", s1Eth100, s2Eth1); + icmpPathintentConstructor( + "192.168.20.1/32", "192.168.20.101/32", s2Eth1, s1Eth100); + + icmpPathintentConstructor( + "192.168.30.101/32", "192.168.30.1/32", s1Eth100, s2Eth1); + icmpPathintentConstructor( + "192.168.30.1/32", "192.168.30.101/32", s2Eth1, s1Eth100); + } + + /** + * Initializes peer connectivity testing environment. + * + * @throws TestUtilsException if exceptions when using TestUtils + */ + private void initPeerConnectivity() throws TestUtilsException { + expect(routingConfig.getBgpPeers()).andReturn(peers).anyTimes(); + expect(bgpConfig.bgpSpeakers()).andReturn(bgpSpeakers).anyTimes(); + replay(bgpConfig); + expect(networkConfigService.getConfig(APPID, BgpConfig.class)).andReturn(bgpConfig).anyTimes(); + replay(networkConfigService); + replay(routingConfig); + replay(interfaceService); + + intentService = createMock(IntentService.class); + replay(intentService); + + intentSynchronizer = new IntentSynchronizer(APPID, intentService, + null, routingConfig, + interfaceService); + intentSynchronizer.leaderChanged(true); + TestUtils.setField(intentSynchronizer, "isActivatedLeader", true); + + peerConnectivityManager = + new PeerConnectivityManager(APPID, intentSynchronizer, + networkConfigService, + CONFIG_APP_ID, + interfaceService); + } + + /** + * Tests whether peer connectivity manager can set up correct BGP and + * ICMP intents according to specific configuration. + * <p/> + * Two tricky cases included in the configuration are: 2 peers on a same + * switch port, peer on the same switch with BGPd. + */ + @Test + public void testConnectionSetup() { + + reset(intentService); + + // Setup the expected intents + for (Intent intent : intentList) { + intentService.submit(eqExceptId(intent)); + } + replay(intentService); + + // Running the interface to be tested. + peerConnectivityManager.start(); + + verify(intentService); + + } + + /** + * Tests a corner case, when there are no interfaces in the configuration. + */ + @Test + public void testNullInterfaces() { + reset(interfaceService); + + expect(interfaceService.getInterfaces()).andReturn( + Sets.<Interface>newHashSet()).anyTimes(); + expect(interfaceService.getInterfacesByPort(s2Eth1)) + .andReturn(Collections.emptySet()).anyTimes(); + expect(interfaceService.getInterfacesByPort(s1Eth1)) + .andReturn(Collections.emptySet()).anyTimes(); + expect(interfaceService.getInterfacesByIp(IpAddress.valueOf("192.168.10.101"))) + .andReturn(Collections.emptySet()).anyTimes(); + expect(interfaceService.getMatchingInterface(IpAddress.valueOf("192.168.10.1"))) + .andReturn(null).anyTimes(); + expect(interfaceService.getInterfacesByIp(IpAddress.valueOf("192.168.20.101"))) + .andReturn(Collections.emptySet()).anyTimes(); + expect(interfaceService.getMatchingInterface(IpAddress.valueOf("192.168.20.1"))) + .andReturn(null).anyTimes(); + expect(interfaceService.getInterfacesByIp(IpAddress.valueOf("192.168.30.101"))) + .andReturn(Collections.emptySet()).anyTimes(); + expect(interfaceService.getMatchingInterface(IpAddress.valueOf("192.168.30.1"))) + .andReturn(null).anyTimes(); + + replay(interfaceService); + + reset(intentService); + replay(intentService); + peerConnectivityManager.start(); + verify(intentService); + } + + /** + * Tests a corner case, when there is no BGP speakers in the configuration. + */ + @Test + public void testNullBgpSpeakers() { + reset(routingConfig); + reset(bgpConfig); + + expect(bgpConfig.bgpSpeakers()).andReturn(Collections.emptySet()).anyTimes(); + replay(bgpConfig); + expect(routingConfig.getBgpPeers()).andReturn(peers).anyTimes(); + replay(routingConfig); + + reset(intentService); + replay(intentService); + peerConnectivityManager.start(); + verify(intentService); + } + + /** + * Tests a corner case, when there is no Interface configured for one BGP + * peer. + */ + @Test + public void testNoPeerInterface() { + String peerSw100Eth1 = "192.168.200.1"; + configuredPeers.put(IpAddress.valueOf(peerSw100Eth1), + new BgpPeer("00:00:00:00:00:00:01:00", 1, peerSw100Eth1)); + testConnectionSetup(); + } + + /** + * Tests a corner case, when there is no Interface configured for one BGP + * speaker. + */ + @Ignore + @Test + public void testNoSpeakerInterface() { + BgpSpeaker bgpSpeaker100 = new BgpSpeaker( + "bgpSpeaker100", + "00:00:00:00:00:00:01:00", 100, + "00:00:00:00:01:00"); + List<InterfaceAddress> interfaceAddresses100 = new LinkedList<>(); + interfaceAddresses100.add(new InterfaceAddress(dpid1, 1, "192.168.10.201")); + interfaceAddresses100.add(new InterfaceAddress(dpid2, 1, "192.168.20.201")); + bgpSpeaker100.setInterfaceAddresses(interfaceAddresses100); + testConnectionSetup(); + } +} diff --git a/framework/src/onos/apps/sdnip/src/test/java/org/onosproject/sdnip/TestIntentServiceHelper.java b/framework/src/onos/apps/sdnip/src/test/java/org/onosproject/sdnip/TestIntentServiceHelper.java new file mode 100644 index 00000000..69b18aa9 --- /dev/null +++ b/framework/src/onos/apps/sdnip/src/test/java/org/onosproject/sdnip/TestIntentServiceHelper.java @@ -0,0 +1,95 @@ +/* + * Copyright 2014-2015 Open Networking Laboratory + * + * Licensed 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.onosproject.sdnip; + +import org.easymock.IArgumentMatcher; +import org.onosproject.net.intent.Intent; +import org.onosproject.sdnip.IntentSynchronizer.IntentKey; + +import static org.easymock.EasyMock.reportMatcher; + +/** + * Helper class for testing operations submitted to the IntentService. + */ +public final class TestIntentServiceHelper { + /** + * Default constructor to prevent instantiation. + */ + private TestIntentServiceHelper() { + } + + /** + * Matcher method to set the expected intent to match against + * (ignoring the intent ID for the intent). + * + * @param intent the expected Intent + * @return the submitted Intent + */ + static Intent eqExceptId(Intent intent) { + reportMatcher(new IdAgnosticIntentMatcher(intent)); + return intent; + } + + /* + * EasyMock matcher that matches {@link Intent} but + * ignores the {@link IntentId} when matching. + * <p/> + * The normal intent equals method tests that the intent IDs are equal, + * however in these tests we can't know what the intent IDs will be in + * advance, so we can't set up expected intents with the correct IDs. Thus, + * the solution is to use an EasyMock matcher that verifies that all the + * value properties of the provided intent match the expected values, but + * ignores the intent ID when testing equality. + * + * FIXME this currently does not take key into account + */ + private static final class IdAgnosticIntentMatcher implements + IArgumentMatcher { + + private final Intent intent; + private String providedString; + + /** + * Constructor taking the expected intent to match against. + * + * @param intent the expected intent + */ + public IdAgnosticIntentMatcher(Intent intent) { + this.intent = intent; + } + + @Override + public void appendTo(StringBuffer strBuffer) { + strBuffer.append("IntentMatcher unable to match: " + + providedString); + } + + @Override + public boolean matches(Object object) { + if (!(object instanceof Intent)) { + return false; + } + + Intent providedIntent = (Intent) object; + providedString = providedIntent.toString(); + + IntentKey thisIntentKey = new IntentKey(intent); + IntentKey providedIntentKey = new IntentKey(providedIntent); + return thisIntentKey.equals(providedIntentKey); + } + } + +} |