Pingping
Committed by Pavlin Radoslavov

port SdnIpTest.java to onos-next

Change-Id: Iec9de810b168e3fbc8f1aa447778d3883fba03a1
package org.onlab.onos.sdnip;
import static org.easymock.EasyMock.createMock;
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.expectLastCall;
import static org.easymock.EasyMock.replay;
import static org.easymock.EasyMock.reset;
import static org.easymock.EasyMock.verify;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.easymock.IAnswer;
import org.junit.Before;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.onlab.junit.IntegrationTest;
import org.onlab.junit.TestUtils;
import org.onlab.junit.TestUtils.TestUtilsException;
import org.onlab.onos.core.ApplicationId;
import org.onlab.onos.net.ConnectPoint;
import org.onlab.onos.net.DeviceId;
import org.onlab.onos.net.PortNumber;
import org.onlab.onos.net.flow.DefaultTrafficSelector;
import org.onlab.onos.net.flow.DefaultTrafficTreatment;
import org.onlab.onos.net.flow.TrafficSelector;
import org.onlab.onos.net.flow.TrafficTreatment;
import org.onlab.onos.net.host.HostService;
import org.onlab.onos.net.host.InterfaceIpAddress;
import org.onlab.onos.net.intent.IntentService;
import org.onlab.onos.net.intent.MultiPointToSinglePointIntent;
import org.onlab.onos.sdnip.config.BgpPeer;
import org.onlab.onos.sdnip.config.Interface;
import org.onlab.onos.sdnip.config.SdnIpConfigService;
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 com.google.common.collect.Sets;
/**
* Integration tests for the SDN-IP application.
* <p/>
* The tests are very coarse-grained. They feed route updates in to
* {@link Router} (simulating routes learnt from iBGP module inside SDN-IP
* application), then they check that the correct intents are created and
* submitted to the intent service. The entire route processing logic of
* Router class is tested.
*/
@Category(IntegrationTest.class)
public class SdnIpTest {
private static final int MAC_ADDRESS_LENGTH = 6;
private static final int MIN_PREFIX_LENGTH = 1;
private static final int MAX_PREFIX_LENGTH = 32;
static Router router;
private SdnIpConfigService sdnIpConfigService;
private InterfaceService interfaceService;
private HostService hostService;
private IntentService intentService;
private Map<IpAddress, BgpPeer> bgpPeers;
private Random random;
static final ConnectPoint SW1_ETH1 = new ConnectPoint(
DeviceId.deviceId("of:0000000000000001"),
PortNumber.portNumber(1));
static final ConnectPoint SW2_ETH1 = new ConnectPoint(
DeviceId.deviceId("of:0000000000000002"),
PortNumber.portNumber(1));
static final ConnectPoint SW3_ETH1 = new ConnectPoint(
DeviceId.deviceId("of:0000000000000003"),
PortNumber.portNumber(1));
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 {
setUpInterfaceService();
setUpSdnIpConfigService();
hostService = new TestHostService();
intentService = createMock(IntentService.class);
random = new Random();
router = new Router(APPID, intentService, hostService,
sdnIpConfigService, interfaceService);
}
/**
* Sets up InterfaceService and virtual {@link Interface}s.
*/
private void setUpInterfaceService() {
interfaceService = createMock(InterfaceService.class);
Set<Interface> interfaces = Sets.newHashSet();
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"));
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"));
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"));
interfaces.add(sw3Eth1);
expect(interfaceService.getInterface(SW1_ETH1)).andReturn(
sw1Eth1).anyTimes();
expect(interfaceService.getInterface(SW2_ETH1)).andReturn(
sw2Eth1).anyTimes();
expect(interfaceService.getInterface(SW3_ETH1)).andReturn(
sw3Eth1).anyTimes();
expect(interfaceService.getInterfaces()).andReturn(
interfaces).anyTimes();
replay(interfaceService);
}
/**
* Sets up SdnIpConfigService and BGP peers in external networks.
*/
private void setUpSdnIpConfigService() {
sdnIpConfigService = createMock(SdnIpConfigService.class);
bgpPeers = new HashMap<>();
String peerSw1Eth1 = "192.168.10.1";
bgpPeers.put(IpAddress.valueOf(peerSw1Eth1),
new BgpPeer("00:00:00:00:00:00:00:01", 1, peerSw1Eth1));
String peer1Sw2Eth1 = "192.168.20.1";
bgpPeers.put(IpAddress.valueOf(peer1Sw2Eth1),
new BgpPeer("00:00:00:00:00:00:00:02", 1, peer1Sw2Eth1));
String peer2Sw2Eth1 = "192.168.30.1";
bgpPeers.put(IpAddress.valueOf(peer2Sw2Eth1),
new BgpPeer("00:00:00:00:00:00:00:03", 1, peer2Sw2Eth1));
expect(sdnIpConfigService.getBgpPeers()).andReturn(bgpPeers).anyTimes();
replay(sdnIpConfigService);
}
/**
* Tests adding a set of routes into {@link Router}.
* <p/>
* Random routes are generated and fed in to the route processing
* logic (via processRouteAdd in Router class). We check that the correct
* intents are generated and submitted to our mock intent service.
*
* @throws InterruptedException if interrupted while waiting on a latch
* @throws TestUtilsException if exceptions when using TestUtils
*/
@Test
public void testAddRoutes() throws InterruptedException, TestUtilsException {
int numRoutes = 100;
final CountDownLatch latch = new CountDownLatch(numRoutes);
List<RouteUpdate> routeUpdates = generateRouteUpdates(numRoutes);
// Set up expectation
reset(intentService);
for (RouteUpdate update : routeUpdates) {
IpAddress nextHopAddress = update.routeEntry().nextHop();
// Find out the egress ConnectPoint
ConnectPoint egressConnectPoint = getConnectPoint(nextHopAddress);
MultiPointToSinglePointIntent intent = getIntentForUpdate(update,
generateMacAddress(nextHopAddress),
egressConnectPoint);
intentService.submit(intent);
expectLastCall().andAnswer(new IAnswer<Object>() {
@Override
public Object answer() throws Throwable {
latch.countDown();
return null;
}
}).once();
}
replay(intentService);
router.leaderChanged(true);
TestUtils.setField(router, "isActivatedLeader", true);
// Add route updates
for (RouteUpdate update : routeUpdates) {
router.processRouteAdd(update.routeEntry());
}
latch.await(5000, TimeUnit.MILLISECONDS);
assertEquals(router.getRoutes().size(), numRoutes);
assertEquals(router.getPushedRouteIntents().size(), numRoutes);
verify(intentService);
}
/**
* Tests adding then deleting a set of routes from {@link Router}.
* <p/>
* Random routes are generated and fed in to the route processing
* logic (via processRouteAdd in Router class), and we check that the
* correct intents are generated. We then delete the entire set of routes
* (by feeding updates to processRouteDelete), and check that the correct
* intents are withdrawn from the intent service.
*
* @throws InterruptedException if interrupted while waiting on a latch
* @throws TestUtilsException exceptions when using TestUtils
*/
@Test
public void testDeleteRoutes() throws InterruptedException, TestUtilsException {
int numRoutes = 100;
List<RouteUpdate> routeUpdates = generateRouteUpdates(numRoutes);
final CountDownLatch installCount = new CountDownLatch(numRoutes);
final CountDownLatch deleteCount = new CountDownLatch(numRoutes);
// Set up expectation
reset(intentService);
for (RouteUpdate update : routeUpdates) {
IpAddress nextHopAddress = update.routeEntry().nextHop();
// Find out the egress ConnectPoint
ConnectPoint egressConnectPoint = getConnectPoint(nextHopAddress);
MultiPointToSinglePointIntent intent = getIntentForUpdate(update,
generateMacAddress(nextHopAddress),
egressConnectPoint);
intentService.submit(intent);
expectLastCall().andAnswer(new IAnswer<Object>() {
@Override
public Object answer() throws Throwable {
installCount.countDown();
return null;
}
}).once();
intentService.withdraw(intent);
expectLastCall().andAnswer(new IAnswer<Object>() {
@Override
public Object answer() throws Throwable {
deleteCount.countDown();
return null;
}
}).once();
}
replay(intentService);
router.leaderChanged(true);
TestUtils.setField(router, "isActivatedLeader", true);
// Send the add updates first
for (RouteUpdate update : routeUpdates) {
router.processRouteAdd(update.routeEntry());
}
// Give some time to let the intents be submitted
installCount.await(5000, TimeUnit.MILLISECONDS);
// Send the DELETE updates
for (RouteUpdate update : routeUpdates) {
router.processRouteDelete(update.routeEntry());
}
deleteCount.await(5000, TimeUnit.MILLISECONDS);
assertEquals(0, router.getRoutes().size());
assertEquals(0, router.getPushedRouteIntents().size());
verify(intentService);
}
/**
* This methods generates random route updates.
*
* @param numRoutes the number of route updates to generate
* @return a list of route update
*/
private List<RouteUpdate> generateRouteUpdates(int numRoutes) {
List<RouteUpdate> routeUpdates = new ArrayList<>(numRoutes);
Set<Ip4Prefix> prefixes = new HashSet<>();
for (int i = 0; i < numRoutes; i++) {
Ip4Prefix prefix;
do {
// Generate a random prefix length between MIN_PREFIX_LENGTH
// and MAX_PREFIX_LENGTH
int prefixLength = random.nextInt(
(MAX_PREFIX_LENGTH - MIN_PREFIX_LENGTH) + 1)
+ MIN_PREFIX_LENGTH;
prefix =
Ip4Prefix.valueOf(Ip4Address.valueOf(random.nextInt()),
prefixLength);
// We have to ensure we don't generate the same prefix twice
// (this is quite easy to happen with small prefix lengths).
// TODO:
// The IpPrefix does the comparison using 32 bits length,
// but we need to compare only the prefix length. So I use
// Ip4Prefix for this moment and changed to IpPrefix. This
// can be improved in the future.
} while (prefixes.contains(prefix));
prefixes.add(prefix);
// Randomly select a peer to use as the next hop
BgpPeer nextHop = null;
int peerNumber = random.nextInt(sdnIpConfigService.getBgpPeers()
.size());
int j = 0;
for (BgpPeer peer : sdnIpConfigService.getBgpPeers().values()) {
if (j++ == peerNumber) {
nextHop = peer;
break;
}
}
assertNotNull(nextHop);
RouteUpdate update =
new RouteUpdate(RouteUpdate.Type.UPDATE,
new RouteEntry(prefix,
nextHop.ipAddress()));
routeUpdates.add(update);
}
return routeUpdates;
}
/**
* Generates the MultiPointToSinglePointIntent that should be
* submitted/withdrawn for a particular RouteUpdate.
*
* @param update the RouteUpdate to generate an intent for
* @param nextHopMac a MAC address to use as the dst-mac for the intent
* @param egressConnectPoint the outgoing ConnectPoint for the intent
* @return the generated intent
*/
private MultiPointToSinglePointIntent getIntentForUpdate(RouteUpdate update,
MacAddress nextHopMac, ConnectPoint egressConnectPoint) {
IpPrefix ip4Prefix = update.routeEntry().prefix();
TrafficSelector.Builder selectorBuilder =
DefaultTrafficSelector.builder();
selectorBuilder.matchEthType(Ethernet.TYPE_IPV4).matchIPDst(ip4Prefix);
TrafficTreatment.Builder treatmentBuilder =
DefaultTrafficTreatment.builder();
treatmentBuilder.setEthDst(nextHopMac);
Set<ConnectPoint> ingressPoints = new HashSet<ConnectPoint>();
for (Interface intf : interfaceService.getInterfaces()) {
if (!intf.connectPoint().equals(egressConnectPoint)) {
ConnectPoint srcPort = intf.connectPoint();
ingressPoints.add(srcPort);
}
}
MultiPointToSinglePointIntent intent =
new MultiPointToSinglePointIntent(APPID,
selectorBuilder.build(), treatmentBuilder.build(),
ingressPoints, egressConnectPoint);
return intent;
}
/**
* Generates a MAC address based on an IP address.
* For the test we need MAC addresses but the actual values don't have any
* meaning, so we'll just generate them based on the IP address. This means
* we have a deterministic mapping from IP address to MAC address.
*
* @param ipAddress IP address used to generate a MAC address
* @return generated MAC address
*/
static MacAddress generateMacAddress(IpAddress ipAddress) {
byte[] macAddress = new byte[MAC_ADDRESS_LENGTH];
ByteBuffer bb = ByteBuffer.wrap(macAddress);
// Put the IP address bytes into the lower four bytes of the MAC
// address. Leave the first two bytes set to 0.
bb.position(2);
bb.put(ipAddress.toOctets());
return MacAddress.valueOf(bb.array());
}
/**
* Finds out the ConnectPoint for a BGP peer address.
*
* @param bgpPeerAddress the BGP peer address.
*/
private ConnectPoint getConnectPoint(IpAddress bgpPeerAddress) {
ConnectPoint connectPoint = null;
for (BgpPeer bgpPeer: bgpPeers.values()) {
if (bgpPeer.ipAddress().equals(bgpPeerAddress)) {
connectPoint = bgpPeer.connectPoint();
break;
}
}
return connectPoint;
}
}
package org.onlab.onos.sdnip;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.onlab.onos.net.ConnectPoint;
import org.onlab.onos.net.DefaultHost;
import org.onlab.onos.net.DeviceId;
import org.onlab.onos.net.Host;
import org.onlab.onos.net.HostId;
import org.onlab.onos.net.HostLocation;
import org.onlab.onos.net.host.HostEvent;
import org.onlab.onos.net.host.HostListener;
import org.onlab.onos.net.host.HostService;
import org.onlab.onos.net.host.PortAddresses;
import org.onlab.onos.net.provider.ProviderId;
import org.onlab.onos.sdnip.Router.InternalHostListener;
import org.onlab.packet.IpAddress;
import org.onlab.packet.MacAddress;
import org.onlab.packet.VlanId;
import com.google.common.collect.Sets;
/**
* Test version of the HostService which is used to simulate delays in
* receiving ARP replies, as you would see in a real system due to the time
* it takes to proxy ARP packets to/from the host. Requests are asynchronous,
* and replies may come back to the requestor in a different order than the
* requests were sent, which again you would expect to see in a real system.
*/
public class TestHostService implements HostService {
/**
* The maximum possible delay before an ARP reply is received.
*/
private static final int MAX_ARP_REPLY_DELAY = 30; // milliseconds
/**
* The probability that we already have the MAC address cached when the
* caller calls {@link #getHostsByIp(IpAddress ipAddress)}.
*/
private static final float MAC_ALREADY_KNOWN_PROBABILITY = 0.3f;
private final ScheduledExecutorService replyTaskExecutor;
private final Random random;
/**
* Class constructor.
*/
public TestHostService() {
replyTaskExecutor = Executors.newSingleThreadScheduledExecutor();
random = new Random();
}
/**
* Task used to reply to ARP requests from a different thread. Replies
* usually come on a different thread in the real system, so we need to
* ensure we test this behavior.
*/
private class ReplyTask implements Runnable {
private HostListener listener;
private IpAddress ipAddress;
/**
* Class constructor.
*
* @param listener the client who requests and waits the MAC address
* @param ipAddress the target IP address of the request
*/
public ReplyTask(InternalHostListener listener,
IpAddress ipAddress) {
this.listener = listener;
this.ipAddress = ipAddress;
}
@Override
public void run() {
Host host = getHostsByIp(ipAddress).iterator().next();
HostEvent hostevent =
new HostEvent(HostEvent.Type.HOST_ADDED, host);
listener.event(hostevent);
}
}
@Override
public Set<Host> getHostsByIp(IpAddress ipAddress) {
float replyChance = random.nextFloat();
// We don't care what the attachment point is in the test,
// so for all the hosts, we use a same ConnectPoint.
Host host = new DefaultHost(ProviderId.NONE, HostId.NONE,
SdnIpTest.generateMacAddress(ipAddress), VlanId.NONE,
new HostLocation(SdnIpTest.SW1_ETH1, 1),
Sets.newHashSet(ipAddress));
if (replyChance < MAC_ALREADY_KNOWN_PROBABILITY) {
// Some percentage of the time we already know the MAC address, so
// we reply directly when the requestor asks for the MAC address
return Sets.newHashSet(host);
}
return new HashSet<Host>();
}
@Override
public void startMonitoringIp(IpAddress ipAddress) {
// Randomly select an amount of time to delay the reply coming back to
int delay = random.nextInt(MAX_ARP_REPLY_DELAY);
ReplyTask replyTask = new ReplyTask(
(SdnIpTest.router.new InternalHostListener()), ipAddress);
replyTaskExecutor.schedule(replyTask, delay, TimeUnit.MILLISECONDS);
}
@Override
public int getHostCount() {
return 0;
}
@Override
public Iterable<Host> getHosts() {
return null;
}
@Override
public Host getHost(HostId hostId) {
return null;
}
@Override
public Set<Host> getHostsByVlan(VlanId vlanId) {
return null;
}
@Override
public Set<Host> getHostsByMac(MacAddress mac) {
return null;
}
@Override
public Set<Host> getConnectedHosts(ConnectPoint connectPoint) {
return null;
}
@Override
public Set<Host> getConnectedHosts(DeviceId deviceId) {
return null;
}
@Override
public void stopMonitoringIp(IpAddress ip) {
}
@Override
public void requestMac(IpAddress ip) {
}
@Override
public Set<PortAddresses> getAddressBindings() {
return null;
}
@Override
public Set<PortAddresses> getAddressBindingsForPort(ConnectPoint connectPoint) {
return null;
}
@Override
public void addListener(HostListener listener) {
}
@Override
public void removeListener(HostListener listener) {
}
}
......@@ -42,6 +42,10 @@
<groupId>org.onlab.onos</groupId>
<artifactId>onlab-misc</artifactId>
</dependency>
<dependency>
<groupId>org.onlab.onos</groupId>
<artifactId>onlab-junit</artifactId>
</dependency>
</dependencies>
<build>
......
......@@ -388,6 +388,7 @@
<redirectTestOutputToFile>true
</redirectTestOutputToFile>
<printSummary>true</printSummary>
<excludedGroups>org.onlab.junit.IntegrationTest</excludedGroups>
</configuration>
</plugin>
......
......@@ -49,6 +49,11 @@
</dependency>
<dependency>
<groupId>org.onlab.onos</groupId>
<artifactId>onlab-junit</artifactId>
</dependency>
<dependency>
<groupId>org.apache.felix</groupId>
<artifactId>org.apache.felix.scr.annotations</artifactId>
</dependency>
......
package org.onlab.junit;
/**
* Marker interface used to separate unit tests from integration tests. All
* integration tests should be marked with:
* {@literal @Category}(IntegrationTest.class)
* so that they can be run separately.
*/
public interface IntegrationTest {
}
......@@ -57,6 +57,11 @@
<artifactId>onlab-rest</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.onlab.onos</groupId>
<artifactId>onlab-junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
......