package net.onrc.onos.apps.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.reportMatcher; 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.net.InetAddress; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; 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 net.floodlightcontroller.core.module.FloodlightModuleContext; import net.floodlightcontroller.core.module.FloodlightModuleException; import net.floodlightcontroller.util.MACAddress; import net.onrc.onos.api.newintent.IntentId; import net.onrc.onos.api.newintent.IntentService; import net.onrc.onos.api.newintent.MultiPointToSinglePointIntent; import net.onrc.onos.apps.proxyarp.IProxyArpService; import net.onrc.onos.apps.sdnip.RibUpdate.Operation; import net.onrc.onos.core.matchaction.action.ModifyDstMacAction; import net.onrc.onos.core.matchaction.match.PacketMatch; import net.onrc.onos.core.matchaction.match.PacketMatchBuilder; import net.onrc.onos.core.registry.IControllerRegistryService; import net.onrc.onos.core.util.IPv4; import net.onrc.onos.core.util.IdBlock; import net.onrc.onos.core.util.IntegrationTest; import net.onrc.onos.core.util.SwitchPort; import net.onrc.onos.core.util.TestUtils; import org.easymock.IAnswer; import org.easymock.IArgumentMatcher; import org.junit.Before; import org.junit.Test; import org.junit.experimental.categories.Category; import com.google.common.net.InetAddresses; /** * Integration tests for the SDN-IP application. * <p/> * The tests are very coarse-grained. They feed route updates in to SDN-IP * (simulating routes learnt from BGPd), then they check that the correct * intents are created and submitted to the intent service. The entire route * processing logic of SDN-IP is tested. */ @Category(IntegrationTest.class) public class SdnIpTest { private static final int MAC_ADDRESS_LENGTH = 6; private static final InetAddress ROUTER_ID = InetAddresses.forString("192.168.10.101"); private static final int MIN_PREFIX_LENGTH = 1; private static final int MAX_PREFIX_LENGTH = 32; private SdnIp sdnip; private IProxyArpService proxyArp; private IntentService intentService; private Map<String, Interface> interfaces; private Map<InetAddress, BgpPeer> peers; private Random random; @Before public void setUp() throws Exception { interfaces = setUpInterfaces(); peers = setUpPeers(); random = new Random(); initSdnIp(); } private Map<String, Interface> setUpInterfaces() { Map<String, Interface> configuredInterfaces = new HashMap<>(); String name1 = "s1-eth1"; configuredInterfaces.put(name1, new Interface(name1, "00:00:00:00:00:00:00:01", (short) 1, "192.168.10.101", 24)); String name2 = "s2-eth1"; configuredInterfaces.put(name2, new Interface(name2, "00:00:00:00:00:00:00:02", (short) 1, "192.168.20.101", 24)); String name3 = "s3-eth1"; configuredInterfaces.put(name3, new Interface(name3, "00:00:00:00:00:00:00:03", (short) 1, "192.168.30.101", 24)); return configuredInterfaces; } private Map<InetAddress, BgpPeer> setUpPeers() { Map<InetAddress, BgpPeer> configuredPeers = new LinkedHashMap<>(); String peer1 = "192.168.10.1"; configuredPeers.put(InetAddresses.forString(peer1), new BgpPeer("s1-eth1", peer1)); String peer2 = "192.168.20.1"; configuredPeers.put(InetAddresses.forString(peer2), new BgpPeer("s2-eth1", peer2)); String peer3 = "192.168.30.1"; configuredPeers.put(InetAddresses.forString(peer3), new BgpPeer("s3-eth1", peer3)); return configuredPeers; } private void initSdnIp() throws FloodlightModuleException { sdnip = new SdnIp(); FloodlightModuleContext context = new FloodlightModuleContext(); context.addConfigParam(sdnip, "BgpdRestIp", "1.1.1.1"); context.addConfigParam(sdnip, "RouterId", "192.168.10.101"); intentService = createMock(IntentService.class); replay(intentService); proxyArp = new TestProxyArpService(); context.addService(IProxyArpService.class, proxyArp); IControllerRegistryService registry = createMock(IControllerRegistryService.class); expect(registry.allocateUniqueIdBlock()). andReturn(new IdBlock(0, Long.MAX_VALUE)); replay(registry); context.addService(IControllerRegistryService.class, registry); TestUtils.setField(sdnip, "intentService", intentService); sdnip.init(context); TestUtils.setField(sdnip, "interfaces", interfaces); TestUtils.setField(sdnip, "bgpPeers", peers); } /** * EasyMock matcher that matches {@link MultiPointToSinglePointIntent}s but * ignores the {@link IntentId} when matching. * <p/> * The normal intent equals method tests that the intent IDs are equal, * however in these tests I can't know what the intent IDs will be in * advance, so I can't set up expected intents with the correct IDs. Even * though I can set up an ID generator that generates a sequential stream * of IDs, I don't know in which order the IDs will be assigned to intents * (because intents can be processed out of order due to timing randomness * introduced by the test ARP module). * <p/> * 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. */ private static final class IdAgnosticIntentMatcher implements IArgumentMatcher { private final MultiPointToSinglePointIntent intent; private String providedIntentString; /** * Constructor taking the expected intent to match against. * * @param intent the expected intent */ public IdAgnosticIntentMatcher(MultiPointToSinglePointIntent intent) { this.intent = intent; } @Override public void appendTo(StringBuffer strBuffer) { strBuffer.append("IntentMatcher unable to match: " + providedIntentString); } @Override public boolean matches(Object object) { if (!(object instanceof MultiPointToSinglePointIntent)) { return false; } MultiPointToSinglePointIntent providedIntent = (MultiPointToSinglePointIntent) object; providedIntentString = providedIntent.toString(); MultiPointToSinglePointIntent matchIntent = new MultiPointToSinglePointIntent(providedIntent.getId(), intent.getMatch(), intent.getAction(), intent.getIngressPorts(), intent.getEgressPort()); return matchIntent.equals(providedIntent); } } /** * Matcher method to set an expected intent to match against (ignoring the * the intent ID). * * @param intent the expected intent * @return something of type MultiPointToSinglePointIntent */ private static MultiPointToSinglePointIntent eqExceptId( MultiPointToSinglePointIntent intent) { reportMatcher(new IdAgnosticIntentMatcher(intent)); return null; } /** * Tests adding a set of routes into SDN-IP. * <p/> * Random routes are generated and fed in to the SDN-IP route processing * logic (via processRibAdd). We check that the correct intents are * generated and submitted to our mock intent service. * * @throws InterruptedException if interrupted while waiting on a latch */ @Test public void testAddRoutes() throws InterruptedException { int numRoutes = 100; final CountDownLatch latch = new CountDownLatch(numRoutes); List<RibUpdate> routeUpdates = generateRouteUpdateIntents(numRoutes); reset(intentService); // Set up expectations for (RibUpdate update : routeUpdates) { InetAddress nextHopPeer = update.getRibEntry().getNextHop(); MultiPointToSinglePointIntent intent = getIntentForUpdate(update, generateMacAddress(nextHopPeer), interfaces.get(peers.get(nextHopPeer).getInterfaceName())); intentService.submit(eqExceptId(intent)); expectLastCall().andAnswer(new IAnswer<Object>() { @Override public Object answer() throws Throwable { latch.countDown(); return null; } }).once(); } replay(intentService); // Add route updates for (RibUpdate update : routeUpdates) { sdnip.processRibAdd(update); } latch.await(5000, TimeUnit.MILLISECONDS); assertEquals(sdnip.getPtree().size(), numRoutes); verify(intentService); } /** * Tests adding then deleting a set of routes from SDN-IP. * <p/> * Random routes are generated and fed in to the SDN-IP route processing * logic (via processRibAdd), and we check that the correct intents are * generated. We then delete the entire set of routes (by feeding updates * to processRibDelete), and check that the correct intents are withdrawn * from the intent service. * * @throws InterruptedException if interrupted while waiting on a latch */ @Test public void testDeleteRoutes() throws InterruptedException { int numRoutes = 100; List<RibUpdate> routeUpdates = generateRouteUpdateIntents(numRoutes); final CountDownLatch installCount = new CountDownLatch(numRoutes); final CountDownLatch deleteCount = new CountDownLatch(numRoutes); reset(intentService); for (RibUpdate update : routeUpdates) { InetAddress nextHopPeer = update.getRibEntry().getNextHop(); MultiPointToSinglePointIntent intent = getIntentForUpdate(update, generateMacAddress(nextHopPeer), interfaces.get(peers.get(nextHopPeer).getInterfaceName())); intentService.submit(eqExceptId(intent)); expectLastCall().andAnswer(new IAnswer<Object>() { @Override public Object answer() throws Throwable { installCount.countDown(); return null; } }).once(); intentService.withdraw(eqExceptId(intent)); expectLastCall().andAnswer(new IAnswer<Object>() { @Override public Object answer() throws Throwable { deleteCount.countDown(); return null; } }).once(); } replay(intentService); // Send the add updates first for (RibUpdate update : routeUpdates) { sdnip.processRibAdd(update); } // Give some time to let the intents be submitted installCount.await(5000, TimeUnit.MILLISECONDS); // Send the DELETE updates for (RibUpdate update : routeUpdates) { RibUpdate deleteUpdate = new RibUpdate(Operation.DELETE, update.getPrefix(), update.getRibEntry()); sdnip.processRibDelete(deleteUpdate); } deleteCount.await(5000, TimeUnit.MILLISECONDS); assertEquals(0, sdnip.getPtree().size()); verify(intentService); } /** * Generates a set of route updates. The prefix for each route is randomly * generated, and the next hop is selected from the set of BGP peers that * was generated during {@link #setUp()}. All have the UPDATE operation. * The generated prefixes are unique within the batch generated by each * call of this method. * * @param numRoutes the number of route updates to generate * @return a list of generated route updates */ private List<RibUpdate> generateRouteUpdateIntents(int numRoutes) { List<RibUpdate> routeUpdates = new ArrayList<>(numRoutes); Set<Prefix> prefixes = new HashSet<>(); for (int i = 0; i < numRoutes; i++) { Prefix prefix; do { InetAddress prefixAddress = InetAddresses.fromInteger(random.nextInt()); // 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 = new Prefix(prefixAddress.getAddress(), prefixLength); // We have to ensure we don't generate the same prefix twice // (this is quite easy to do with small prefix lengths) } while (prefixes.contains(prefix)); prefixes.add(prefix); // Randomly select a peer to use as the next hop BgpPeer nextHop = null; int peerNumber = random.nextInt(peers.size()); int j = 0; for (BgpPeer peer : peers.values()) { if (j++ == peerNumber) { nextHop = peer; break; } } assertNotNull(nextHop); RibUpdate update = new RibUpdate(Operation.UPDATE, prefix, new RibEntry(ROUTER_ID, nextHop.getIpAddress())); routeUpdates.add(update); } return routeUpdates; } /** * Generates the MultiPointToSinglePointIntent that should be * submitted/withdrawn for a particular RibUpdate. * * @param update the RibUpdate to generate an intent for * @param nextHopMac a MAC address to use as the dst-mac for the intent * @param egressInterface the outgoing interface for the intent * @return the generated intent */ private MultiPointToSinglePointIntent getIntentForUpdate(RibUpdate update, MACAddress nextHopMac, Interface egressInterface) { Prefix prefix = update.getPrefix(); PacketMatchBuilder builder = new PacketMatchBuilder(); builder.setDstIp(new IPv4( InetAddresses.coerceToInteger(prefix.getInetAddress())), (short) prefix.getPrefixLength()); PacketMatch match = builder.build(); ModifyDstMacAction action = new ModifyDstMacAction(nextHopMac); Set<SwitchPort> ingressPorts = new HashSet<>(); for (Interface intf : interfaces.values()) { if (!intf.equals(egressInterface)) { ingressPorts.add(intf.getSwitchPort()); } } // Create the intent. The intent ID is arbitrary because we don't consider // it when matching against the intent passed to the mock MultiPointToSinglePointIntent intent = new MultiPointToSinglePointIntent( new IntentId(0), match, action, ingressPorts, egressInterface.getSwitchPort()); 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 */ public static MACAddress generateMacAddress(InetAddress 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.getAddress()); return MACAddress.valueOf(bb.array()); } }