diff --git a/src/main/java/net/floodlightcontroller/core/internal/Controller.java b/src/main/java/net/floodlightcontroller/core/internal/Controller.java index b7b048874a874b508e327bd87998832898f2c93f..6b183ca95d2dcd960e70f4d32b6b12bf812e7916 100644 --- a/src/main/java/net/floodlightcontroller/core/internal/Controller.java +++ b/src/main/java/net/floodlightcontroller/core/internal/Controller.java @@ -180,6 +180,13 @@ public class Controller implements IFloodlightProviderService, // The current role of the controller. // If the controller isn't configured to support roles, then this is null. protected Role role; + // This is the role of the controller based on HARoleChange notifications + // we have sent. I.e., this field reflects the last role notification + // we have sent to the listeners. On a transition to slave we first set + // this role and then notify, on a transition to master we first notify + // and then set the role. We then use it to make sure we don't forward + // OF messages while the modules are in slave role. + protected volatile Role notifiedRole; // A helper that handles sending and timeout handling for role requests protected RoleChanger roleChanger; @@ -286,11 +293,19 @@ public class Controller implements IFloodlightProviderService, log.trace("Dispatching HA Role update newRole = {}, oldRole = {}", newRole, oldRole); } + // Set notified role to slave before notifying listeners. This + // stops OF messages from being sent to listeners + if (newRole == Role.SLAVE) + Controller.this.notifiedRole = newRole; if (haListeners != null) { for (IHAListener listener : haListeners) { listener.roleChanged(oldRole, newRole); } } + // Set notified role to master/equal after notifying listeners. + // We now forward messages again + if (newRole != Role.SLAVE) + Controller.this.notifiedRole = newRole; } } @@ -981,7 +996,7 @@ public class Controller implements IFloodlightProviderService, shouldHandleMessage = true; } else { // Queue till we complete driver binding - state.queuedOFMessages.add((OFPortStatus) m); + state.queuedOFMessages.add(m); } break; @@ -1001,9 +1016,9 @@ public class Controller implements IFloodlightProviderService, try { if (sw.isConnected()) { - // Check if the controller is in the slave role for the - // switch. If it is, then don't dispatch the message to - // the listeners. + // Only dispatch message if the switch is in the + // activeSwitch map and if the switches role is + // not slave and the modules are not in slave // TODO: Should we dispatch messages that we expect to // receive when we're in the slave role, e.g. port // status messages? Since we're "hiding" switches from @@ -1012,7 +1027,9 @@ public class Controller implements IFloodlightProviderService, // to them. On the other hand there might be special // modules that care about all of the connected switches // and would like to receive port status notifications. - if (sw.getHARole() == Role.SLAVE) { + if (sw.getHARole() == Role.SLAVE || + notifiedRole == Role.SLAVE || + !activeSwitches.containsKey(sw.getId())) { // Don't log message if it's a port status message // since we expect to receive those from the switch // and don't want to emit spurious messages. @@ -1121,7 +1138,7 @@ public class Controller implements IFloodlightProviderService, FloodlightContext bContext) throws IOException { Ethernet eth = null; - + switch (m.getType()) { case PACKET_IN: OFPacketIn pi = (OFPacketIn)m; @@ -1742,6 +1759,7 @@ public class Controller implements IFloodlightProviderService, this.providerMap = new HashMap<String, List<IInfoProvider>>(); setConfigParams(configParams); this.role = getInitialRole(configParams); + this.notifiedRole = this.role; this.roleChanger = new RoleChanger(this); initVendorMessages(); this.systemStartTime = System.currentTimeMillis(); diff --git a/src/test/java/net/floodlightcontroller/core/internal/ControllerTest.java b/src/test/java/net/floodlightcontroller/core/internal/ControllerTest.java index 195f1c042cfcf2f90878ae7ced4f773eb1fdb998..6879f3f13a93ced3a2b08e41dba11d6fd84edf11 100644 --- a/src/test/java/net/floodlightcontroller/core/internal/ControllerTest.java +++ b/src/test/java/net/floodlightcontroller/core/internal/ControllerTest.java @@ -18,7 +18,6 @@ package net.floodlightcontroller.core.internal; import static org.easymock.EasyMock.*; - import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -29,15 +28,16 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; -import net.floodlightcontroller.core.FloodlightProvider; import net.floodlightcontroller.core.FloodlightContext; +import net.floodlightcontroller.core.FloodlightProvider; import net.floodlightcontroller.core.IFloodlightProviderService; -import net.floodlightcontroller.core.IHAListener; import net.floodlightcontroller.core.IFloodlightProviderService.Role; +import net.floodlightcontroller.core.IHAListener; +import net.floodlightcontroller.core.IListener.Command; import net.floodlightcontroller.core.IOFMessageFilterManagerService; import net.floodlightcontroller.core.IOFMessageListener; -import net.floodlightcontroller.core.IListener.Command; import net.floodlightcontroller.core.IOFSwitch; import net.floodlightcontroller.core.IOFSwitchDriver; import net.floodlightcontroller.core.IOFSwitchListener; @@ -72,15 +72,16 @@ import org.junit.Test; import org.openflow.protocol.OFError; import org.openflow.protocol.OFError.OFErrorType; import org.openflow.protocol.OFFeaturesReply; +import org.openflow.protocol.OFMessage; import org.openflow.protocol.OFPacketIn; +import org.openflow.protocol.OFPacketIn.OFPacketInReason; import org.openflow.protocol.OFPacketOut; import org.openflow.protocol.OFPhysicalPort; import org.openflow.protocol.OFPort; import org.openflow.protocol.OFPortStatus; +import org.openflow.protocol.OFPortStatus.OFPortReason; import org.openflow.protocol.OFStatisticsReply; import org.openflow.protocol.OFType; -import org.openflow.protocol.OFPacketIn.OFPacketInReason; -import org.openflow.protocol.OFPortStatus.OFPortReason; import org.openflow.protocol.OFVendor; import org.openflow.protocol.action.OFAction; import org.openflow.protocol.action.OFActionOutput; @@ -172,6 +173,7 @@ public class ControllerTest extends FloodlightTestCase * Run the controller's main loop so that updates are processed */ protected class ControllerRunThread extends Thread { + @Override public void run() { controller.openFlowPort = 0; // Don't listen controller.run(); @@ -540,14 +542,17 @@ public class ControllerTest extends FloodlightTestCase nRemoved = 0; nPortChanged = 0; } + @Override public synchronized void addedSwitch(IOFSwitch sw) { nAdded++; notifyAll(); } + @Override public synchronized void removedSwitch(IOFSwitch sw) { nRemoved++; notifyAll(); } + @Override public String getName() { return "dummy"; } @@ -1349,5 +1354,356 @@ public class ControllerTest extends FloodlightTestCase } return null; } + + private void setupSwitchForDispatchTest(IOFSwitch sw, + boolean isConnected, + Role role) { + Lock lock = createNiceMock(Lock.class); + expect(sw.getId()).andReturn(1L).anyTimes(); + expect(sw.getStringId()).andReturn("00:00:00:00:00:01").anyTimes(); + expect(sw.getListenerReadLock()).andReturn(lock).anyTimes(); + expect(sw.isConnected()).andReturn(isConnected).anyTimes(); + expect(sw.getHARole()).andReturn(role).anyTimes(); + replay(lock); + + } + + @Test + public void testMessageDispatch() throws Exception { + // Mock a dummy packet in + // Build our test packet + IPacket testPacket = new Ethernet() + .setSourceMACAddress("00:44:33:22:11:00") + .setDestinationMACAddress("00:11:22:33:44:55") + .setEtherType(Ethernet.TYPE_ARP) + .setPayload( + new ARP() + .setHardwareType(ARP.HW_TYPE_ETHERNET) + .setProtocolType(ARP.PROTO_TYPE_IP) + .setHardwareAddressLength((byte) 6) + .setProtocolAddressLength((byte) 4) + .setOpCode(ARP.OP_REPLY) + .setSenderHardwareAddress(Ethernet.toMACAddress("00:44:33:22:11:00")) + .setSenderProtocolAddress(IPv4.toIPv4AddressBytes("192.168.1.1")) + .setTargetHardwareAddress(Ethernet.toMACAddress("00:11:22:33:44:55")) + .setTargetProtocolAddress(IPv4.toIPv4AddressBytes("192.168.1.2"))); + byte[] testPacketSerialized = testPacket.serialize(); + + // Build the PacketIn + OFPacketIn pi = ((OFPacketIn) new BasicFactory().getMessage(OFType.PACKET_IN)) + .setBufferId(-1) + .setInPort((short) 1) + .setPacketData(testPacketSerialized) + .setReason(OFPacketInReason.NO_MATCH) + .setTotalLength((short) testPacketSerialized.length); + + + // Mock switch and add to data structures + IOFSwitch sw = createMock(IOFSwitch.class); + + controller.connectedSwitches.add(sw); + + // create a channel handler + OFChannelState state = new OFChannelState(); + state.hsState = HandshakeState.READY; + Controller.OFChannelHandler chdlr = controller.new OFChannelHandler(state); + chdlr.sw = sw; + + // mock role changer + RoleChanger roleChanger = createMock(RoleChanger.class); + roleChanger.submitRequest(eq(controller.connectedSwitches), + anyObject(Role.class)); + expectLastCall().anyTimes(); + controller.roleChanger = roleChanger; + + + // Mock message listener and add + IOFMessageListener listener = createNiceMock(IOFMessageListener.class); + expect(listener.getName()).andReturn("foobar").anyTimes(); + replay(listener); + controller.addOFMessageListener(OFType.PACKET_IN, listener); + resetToStrict(listener); + + + assertEquals("Check that update queue is empty", 0, + controller.updates.size()); + + + replay(roleChanger); + + //------------------- + // Test 1: role is master, switch is master and in activeMap + // we expect the msg to be dispatched + reset(sw); + resetToDefault(listener); + controller.activeSwitches.put(1L, sw); + setupSwitchForDispatchTest(sw, true, Role.MASTER); + listener.receive(same(sw), same(pi), + anyObject(FloodlightContext.class)); + expectLastCall().andReturn(Command.STOP).once(); + replay(sw, listener); + chdlr.processOFMessage(pi); + verify(sw, listener); + assertEquals(0, controller.updates.size()); + + + //------------------- + // Test 1b: role is master, switch is master and in activeMap + // but switch is not connected + // no message dispatched + reset(sw); + resetToDefault(listener); + controller.activeSwitches.put(1L, sw); + setupSwitchForDispatchTest(sw, false, Role.MASTER); + replay(sw, listener); + chdlr.processOFMessage(pi); + verify(sw, listener); + assertEquals(0, controller.updates.size()); + + + //------------------- + // Test 1c: role is master, switch is slave and in activeMap + // no message dispatched + reset(sw); + resetToDefault(listener); + controller.activeSwitches.put(1L, sw); + setupSwitchForDispatchTest(sw, true, Role.SLAVE); + replay(sw, listener); + chdlr.processOFMessage(pi); + verify(sw, listener); + assertEquals(0, controller.updates.size()); + + + //------------------- + // Test 1d: role is master, switch is master but not in activeMap + // we expect the msg to be dispatched + reset(sw); + resetToDefault(listener); + controller.activeSwitches.remove(1L); + setupSwitchForDispatchTest(sw, true, Role.MASTER); + replay(sw, listener); + chdlr.processOFMessage(pi); + verify(sw, listener); + assertEquals(0, controller.updates.size()); + + + + //------------------- + // Test 2: check correct dispatch and HA notification behavior + // We set the role to slave but do not notify the clients + reset(sw); + resetToDefault(listener); + controller.activeSwitches.put(1L, sw); + setupSwitchForDispatchTest(sw, true, Role.MASTER); + listener.receive(same(sw), same(pi), + anyObject(FloodlightContext.class)); + expectLastCall().andReturn(Command.STOP).once(); + replay(sw, listener); + controller.setRole(Role.SLAVE); + chdlr.processOFMessage(pi); + verify(sw, listener); + assertEquals(1, controller.updates.size()); + + // Now notify listeners + Controller.IUpdate upd = controller.updates.poll(1, TimeUnit.NANOSECONDS); + assertTrue("Check that update is HARoleUpdate", + upd instanceof Controller.HARoleUpdate); + upd.dispatch(); + resetToDefault(listener); + replay(listener); + chdlr.processOFMessage(pi); + verify(listener); + assertEquals(0, controller.updates.size()); + + // transition back to master but don't notify yet + resetToDefault(listener); + replay(listener); + controller.setRole(Role.MASTER); + chdlr.processOFMessage(pi); + verify(listener); + assertEquals(1, controller.updates.size()); + + // now notify listeners + upd = controller.updates.poll(1, TimeUnit.NANOSECONDS); + assertTrue("Check that update is HARoleUpdate", + upd instanceof Controller.HARoleUpdate); + upd.dispatch(); + resetToDefault(listener); + listener.receive(same(sw), same(pi), + anyObject(FloodlightContext.class)); + expectLastCall().andReturn(Command.STOP).once(); + replay(listener); + chdlr.processOFMessage(pi); + verify(listener); + assertEquals(0, controller.updates.size()); + + verify(sw); + } + + + /* + * Test correct timing behavior between HA Role notification and dispatching + * OFMessages to listeners. + * When transitioning to SLAVE: stop dispatching message before sending + * notifications + * When transitioning to MASTER: start dispatching messages after sending + * notifications + * (This implies that messages should not be dispatched while the + * notifications are being sent). + * + * We encapsulate the logic for this in a class that implements both + * IHAListener and IOFMessageListener. Then we inject an OFMessage fom + * the IHAListener and check that it gets dropped correctly. + */ + @Test + public void testRoleNotifcationAndMessageDispatch() throws Exception { + class TestRoleNotificationsAndDispatch implements IHAListener, IOFMessageListener { + OFPacketIn pi; + Controller.OFChannelHandler chdlr; + IOFSwitch sw; + private boolean haveReceived; + private boolean doInjectMessageFromHAListener; + + public TestRoleNotificationsAndDispatch() { + IPacket testPacket = new Ethernet() + .setSourceMACAddress("00:44:33:22:11:00") + .setDestinationMACAddress("00:11:22:33:44:55") + .setEtherType(Ethernet.TYPE_ARP) + .setPayload( + new ARP() + .setHardwareType(ARP.HW_TYPE_ETHERNET) + .setProtocolType(ARP.PROTO_TYPE_IP) + .setHardwareAddressLength((byte) 6) + .setProtocolAddressLength((byte) 4) + .setOpCode(ARP.OP_REPLY) + .setSenderHardwareAddress(Ethernet.toMACAddress("00:44:33:22:11:00")) + .setSenderProtocolAddress(IPv4.toIPv4AddressBytes("192.168.1.1")) + .setTargetHardwareAddress(Ethernet.toMACAddress("00:11:22:33:44:55")) + .setTargetProtocolAddress(IPv4.toIPv4AddressBytes("192.168.1.2"))); + byte[] testPacketSerialized = testPacket.serialize(); + + // Build the PacketIn + pi = ((OFPacketIn) new BasicFactory().getMessage(OFType.PACKET_IN)) + .setBufferId(-1) + .setInPort((short) 1) + .setPacketData(testPacketSerialized) + .setReason(OFPacketInReason.NO_MATCH) + .setTotalLength((short) testPacketSerialized.length); + + // Mock switch and add to data structures + sw = createMock(IOFSwitch.class); + controller.connectedSwitches.add(sw); + controller.activeSwitches.put(1L, sw); + setupSwitchForDispatchTest(sw, true, Role.MASTER); + replay(sw); + + // create a channel handler + OFChannelState state = new OFChannelState(); + state.hsState = HandshakeState.READY; + chdlr = controller.new OFChannelHandler(state); + chdlr.sw = this.sw; + + // add ourself as listeners + controller.addOFMessageListener(OFType.PACKET_IN, this); + controller.addHAListener(this); + } + + + private void injectMessage(boolean shouldReceive) throws Exception { + haveReceived = false; + chdlr.processOFMessage(pi); + assertEquals(shouldReceive, haveReceived); + } + + public void transitionToSlave() throws Exception { + IUpdate update; + + // Bring controller into well defined state for MASTER + doInjectMessageFromHAListener = false; + update = controller.new HARoleUpdate(Role.MASTER, Role.SLAVE); + update.dispatch(); + doInjectMessageFromHAListener = true; + + + // inject message. Listener called + injectMessage(true); + // Dispatch update + update = controller.new HARoleUpdate(Role.SLAVE, Role.MASTER); + update.dispatch(); + // inject message. Listener not called + injectMessage(false); + } + + public void transitionToMaster() throws Exception { + IUpdate update; + + // Bring controller into well defined state for SLAVE + doInjectMessageFromHAListener = false; + update = controller.new HARoleUpdate(Role.SLAVE, Role.MASTER); + update.dispatch(); + doInjectMessageFromHAListener = true; + + + // inject message. Listener not called + injectMessage(false); + // Dispatch update + update = controller.new HARoleUpdate(Role.MASTER, Role.SLAVE); + update.dispatch(); + // inject message. Listener called + injectMessage(true); + } + + //--------------- + // IHAListener + //--------------- + @Override + public void roleChanged(Role oldRole, Role newRole) { + try { + if (doInjectMessageFromHAListener) + injectMessage(false); + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + @Override + public + void + controllerNodeIPsChanged(Map<String, String> curControllerNodeIPs, + Map<String, String> addedControllerNodeIPs, + Map<String, String> removedControllerNodeIPs) { + // TODO Auto-generated method stub + } + + //------------------------- + // IOFMessageListener + //------------------------- + @Override + public String getName() { + return "FooBar"; + } + @Override + public boolean isCallbackOrderingPrereq(OFType type, String name) { + return false; + } + @Override + public boolean isCallbackOrderingPostreq(OFType type, String name) { + return false; + } + @Override + public Command receive(IOFSwitch sw, + OFMessage msg, + FloodlightContext cntx) { + haveReceived = true; + return Command.STOP; + } + } + + TestRoleNotificationsAndDispatch x = new TestRoleNotificationsAndDispatch(); + x.transitionToSlave(); + x.transitionToMaster(); + + } }