How to Add Services to a Module (v0.90, v0.91)
|
This tutorial is for Floodlight v0.90 and v0.91 (old master). We recommend you upgrade to Floodlight v1.0 for a better API and to use OpenFlow 1.3+ features. You can find the v1.0 documentation here. |
This document will walk you through the process of adding a new module to Floodlight. Along the way, hopefully you'll learn a thing or two about how the controller architecture works.
Introduction
The controller architecture consists of a core module, that's responsible for listening to the openflow sockets and dispatching events, and a number of secondary modules that can register with the core module to handle those events. When you first start up the controller with debug logging enabled, you'll see a report of all the handlers as they register themselves:
17:29:23.231 [main] DEBUG n.f.core.internal.Controller - OFListeners for PACKET_IN: devicemanager, 17:29:23.231 [main] DEBUG n.f.core.internal.Controller - OFListeners for PORT_STATUS: devicemanager, 17:29:23.237 [main] DEBUG n.f.c.module.FloodlightModuleLoader - Starting net.floodlightcontroller.restserver.RestApiServer 17:29:23.237 [main] DEBUG n.f.c.module.FloodlightModuleLoader - Starting net.floodlightcontroller.forwarding.Forwarding 17:29:23.237 [main] DEBUG n.f.forwarding.Forwarding - Starting net.floodlightcontroller.forwarding.Forwarding 17:29:23.237 [main] DEBUG n.f.core.internal.Controller - OFListeners for PACKET_IN: devicemanager,forwarding, 17:29:23.237 [main] DEBUG n.f.c.module.FloodlightModuleLoader - Starting net.floodlightcontroller.storage.memory.MemoryStorageSource 17:29:23.240 [main] DEBUG n.f.restserver.RestApiServer - Adding REST API routable net.floodlightcontroller.storage.web.StorageWebRoutable 17:29:23.242 [main] DEBUG n.f.c.module.FloodlightModuleLoader - Starting net.floodlightcontroller.core.OFMessageFilterManager 17:29:23.242 [main] DEBUG n.f.core.internal.Controller - OFListeners for PACKET_IN: devicemanager,forwarding,messageFilterManager, 17:29:23.242 [main] DEBUG n.f.core.internal.Controller - OFListeners for PACKET_OUT: messageFilterManager, 17:29:23.242 [main] DEBUG n.f.core.internal.Controller - OFListeners for FLOW_MOD: messageFilterManager, 17:29:23.242 [main] DEBUG n.f.c.module.FloodlightModuleLoader - Starting net.floodlightcontroller.routing.dijkstra.RoutingImpl 17:29:23.247 [main] DEBUG n.f.c.module.FloodlightModuleLoader - Starting net.floodlightcontroller.core.CoreModule 17:29:23.248 [main] DEBUG n.f.core.internal.Controller - Doing controller internal setup 17:29:23.251 [main] INFO n.f.core.internal.Controller - Connected to storage source 17:29:23.252 [main] DEBUG n.f.restserver.RestApiServer - Adding REST API routable net.floodlightcontroller.core.web.CoreWebRoutable 17:29:23.252 [main] DEBUG n.f.c.module.FloodlightModuleLoader - Starting net.floodlightcontroller.topology.internal.TopologyImpl 17:29:23.254 [main] DEBUG n.f.core.internal.Controller - OFListeners for PACKET_IN: topology,devicemanager,forwarding,messageFilterManager, 17:29:23.254 [main] DEBUG n.f.core.internal.Controller - OFListeners for PORT_STATUS: devicemanager,topology,
There are a number of different types of OpenFlow messages for which events are generated, but most of the action happens in the PacketIn handlers. A PacketIn message is the OpenFlow message that is sent from the switch to the controller if the switch does not have a flow table rule that matches the packet. The controller is expected to handle the packet and to install any necessary flows table entries (using a set of FlowMod messages). In this tutorial, we'll be adding a new PacketIn listener will store the PacketIn messages. We will then make these messages available via the REST API.
Creating the Class
Add class in Eclipse
- Expand the "Floodlight" item in the Package Explorer and find the "src/main/java" folder.
- Right-click on the "src/main/java" folder and choose "New/Class."
- Enter "net.floodlightcontroller.pktinhistory" in the "Package" box.
- Enter "PktInHistory" in the "name" box.
- Next to the "Interfaces" box, choose "Add..."
- Type "IFloodlightModule" into the search box, and choose "OK."
- Repeat steps above for "IOFMessageListener"
- Click "Finish" in the dialog.
You should end up with something like this:
package net.floodlightcontroller.pktinhistory; import java.util.Collection; import java.util.Map; import org.openflow.protocol.OFMessage; import org.openflow.protocol.OFType; import net.floodlightcontroller.core.FloodlightContext; import net.floodlightcontroller.core.IOFMessageListener; import net.floodlightcontroller.core.IOFSwitch; import net.floodlightcontroller.core.module.FloodlightModuleContext; import net.floodlightcontroller.core.module.FloodlightModuleException; import net.floodlightcontroller.core.module.IFloodlightModule; import net.floodlightcontroller.core.module.IFloodlightService; public class PktInHistory implements IFloodlightModule, IOFMessageListener { @Override public String getName() { // TODO Auto-generated method stub return null; } @Override public boolean isCallbackOrderingPrereq(OFType type, String name) { // TODO Auto-generated method stub return false; } @Override public boolean isCallbackOrderingPostreq(OFType type, String name) { // TODO Auto-generated method stub return false; } @Override public net.floodlightcontroller.core.IListener.Command receive( IOFSwitch sw, OFMessage msg, FloodlightContext cntx) { // TODO Auto-generated method stub return null; } @Override public Collection<Class<? extends IFloodlightService>> getModuleServices() { // TODO Auto-generated method stub return null; } @Override public Map<Class<? extends IFloodlightService>, IFloodlightService> getServiceImpls() { // TODO Auto-generated method stub return null; } @Override public Collection<Class<? extends IFloodlightService>> getModuleDependencies() { // TODO Auto-generated method stub return null; } @Override public void init(FloodlightModuleContext context) throws FloodlightModuleException { // TODO Auto-generated method stub } @Override public void startUp(FloodlightModuleContext context) { // TODO Auto-generated method stub } }
Setting up Module Dependencies
Now that we have our skeleton class, we have to implement the correct functions to make the module loadable. Since we are listening to OpenFlow messages we need to register with the FloodlightProvider (IFloodlightProviderService class). First we need to add it as a dependency. We create a member variable as so.
protected IFloodlightProviderService floodlightProvider;
Now we need to wire it up from the module loading system. First we have to tell the module loader we depend upon on. For that we'll modify the getModuleDependencies() function.
@Override public Collection<Class<? extends IFloodlightService>> getModuleDependencies() { Collection<Class<? extends IFloodlightService>> l = new ArrayList<Class<? extends IFloodlightService>>(); l.add(IFloodlightProviderService.class); return l; }
Now that we've told the module loader we depend upon the IFloodlightProviderService we have to get the reference to it. We also create our internal datastructure (the Circular Buffer, shown later) here. This is done in the init method.
@Override public void init(FloodlightModuleContext context) throws FloodlightModuleException { floodlightProvider = context.getServiceImpl(IFloodlightProviderService.class); }
Handling OpenFlow messages
Now that's we got a reference to the Floodlight provider we need to tell it we want to handle OpenFlow's PacketIn messages. We do this in the startUp() method.
@Override public void startUp(FloodlightModuleContext context) { floodlightProvider.addOFMessageListener(OFType.PACKET_IN, this); }
We also have to put in our ID for the OFMessage listener. This is done in the getName() call.
@Override public String getName() { return "PktInHistory"; }
For isCallbackOrderingPrereq() and isCallbackOrderingPostreq() calls we'll just leave them to return false. This is saying we do not care what order are in for the PacketIn processing chain.
We create a circular buffer to handle store the Packet In messages. Define the buffer as a member variable.
protected ConcurrentCircularBuffer<SwitchMessagePair> buffer;
Instantiate it in the init method that will now look like this.
@Override public void init(FloodlightModuleContext context) throws FloodlightModuleException { floodlightProvider = context.getServiceImpl(IFloodlightProviderService.class); buffer = new ConcurrentCircularBuffer<SwitchMessagePair>(SwitchMessagePair.class, 100); }
|
You'll need to import the ConcurrentCircularBuffer into the PktInHistory package. It is available here. |
Now we have to define the behavior for what to do when the module receives a PacketIn message. This is done in the receive() function.
@Override public Command receive(IOFSwitch sw, OFMessage msg, FloodlightContext cntx) { switch(msg.getType()) { case PACKET_IN: buffer.add(new SwitchMessagePair(sw, msg)); break; default: break; } return Command.CONTINUE; }
For each PacketIn we will add the message along with the switch it came in on. Returning Command.CONTINUE tells the IFloodlightProvider to pass the PacketIn to the next module in the system. Returning Command.STOP will tell it to stop processing here.
Adding a rest API
We now have a complete module implementation, but no way to get information out of it! For this we'll need to do two things. Have our module export a service then tie it into the REST API Module.
In a nutshell, the steps to add a new Restlet include:
- Registering a Restlet in net.floodlightcontroller.controller.internal.Controller.
- Implement a *WebRoutable class that implements RestletRoutable and provides getRestlet() and basePath() functions.
- Implement a *Resource class that extends ServerResource() and implements a @Get or @Put function.
We'll create an interface called IPktInHistoryService and have it extend IFloodlightService. We use the naming convention of appending Service to any interface that extends IFloodlightService. The service will have one method, getBuffer() to retrieve the circular buffer we use. The interface will look like this.
package net.floodlightcontroller.pktinhistory; import net.floodlightcontroller.core.module.IFloodlightService; import net.floodlightcontroller.core.types.SwitchMessagePair; public interface IPktinHistoryService extends IFloodlightService { public ConcurrentCircularBuffer<SwitchMessagePair> getBuffer(); }
Going back to our PktInHistory module we now need to do a couple things. One is make it implement the IPktInHistoryService. Our class definition will now look like this.
public class PktInHistory implements IFloodlightModule, IPktinHistoryService, IOFMessageListener {
We also have to implement the service's getBuffer() method.
@Override public ConcurrentCircularBuffer<SwitchMessagePair> getBuffer() { return buffer; }
We tell the module system that we provide the IPktInHistoryService. We modify the getModuleServices() and getServiceImpls() methods.
@Override public Collection<Class<? extends IFloodlightService>> getModuleServices() { Collection<Class<? extends IFloodlightService>> l = new ArrayList<Class<? extends IFloodlightService>>(); l.add(IPktinHistoryService.class); return l; } @Override public Map<Class<? extends IFloodlightService>, IFloodlightService> getServiceImpls() { Map<Class<? extends IFloodlightService>, IFloodlightService> m = new HashMap<Class<? extends IFloodlightService>, IFloodlightService>(); m.put(IPktinHistoryService.class, this); return m; }
The getServiceImpls() call tells the module system that we are the class that provides the service.
We also need to get add a reference to the REST API service.
protected IRestApiService restApi;
We'll have to add the IRestApiService as a dependency and wire it up the same way we did with the IFloodlightProviderService. The init() and getModuleDependencies() methods will now look like this.
@Override public Collection<Class<? extends IFloodlightService>> getModuleDependencies() { Collection<Class<? extends IFloodlightService>> l = new ArrayList<Class<? extends IFloodlightService>>(); l.add(IFloodlightProviderService.class); l.add(IRestApiService.class); return l; } @Override public void init(FloodlightModuleContext context) throws FloodlightModuleException { floodlightProvider = context.getServiceImpl(IFloodlightProviderService.class); restApi = context.getServiceImpl(IRestApiService.class); buffer = new ConcurrentCircularBuffer<SwitchMessagePair>(SwitchMessagePair.class, 100); }
Now we create the classes that are used in the REST API. There are two parts. The first is a class that handles a specific URL call and another class that registers this with the REST API.
First we'll create the class that handles the REST API request. When a request comes in it will just return a snapshot of the circular buffer.
package net.floodlightcontroller.pktinhistory; import java.util.ArrayList; import java.util.List; import net.floodlightcontroller.core.types.SwitchMessagePair; import org.restlet.resource.Get; import org.restlet.resource.ServerResource; public class PktInHistoryResource extends ServerResource { @Get("json") public List<SwitchMessagePair> retrieve() { IPktinHistoryService pihr = (IPktinHistoryService)getContext().getAttributes().get(IPktinHistoryService.class.getCanonicalName()); List<SwitchMessagePair> l = new ArrayList<SwitchMessagePair>(); l.addAll(java.util.Arrays.asList(pihr.getBuffer().snapshot())); return l; } }
Now we have to create the RestletRoutable. This tells the REST API we are registering this API and bind's it's URLs to a specific resource. The class looks like this.
package net.floodlightcontroller.pktinhistory; import org.restlet.Context; import org.restlet.Restlet; import org.restlet.routing.Router; import net.floodlightcontroller.restserver.RestletRoutable; public class PktInHistoryWebRoutable implements RestletRoutable { @Override public Restlet getRestlet(Context context) { Router router = new Router(context); router.attach("/history/json", PktInHistoryResource.class); return router; } @Override public String basePath() { return "/wm/pktinhistory"; } }
Now we need to register our Restlet Routable with the REST API service. We'll modify our startUp() method to do that. It will now look like this.
@Override public void startUp(FloodlightModuleContext context) { floodlightProvider.addOFMessageListener(OFType.PACKET_IN, this); restApi.addRestletRoutable(new PktInHistoryWebRoutable()); }
The data is serialized into REST format using Jackson. Jackson will look at a class and try to serialize every field that has a getter. In this case we don't want everything to be serialized in OFSwitchImpl so we'll need to write a custom serializer. We will add it to the net.floodlightcontroller.web.serialzers package.
package net.floodlightcontroller.core.web.serializers; import java.io.IOException; import net.floodlightcontroller.core.internal.OFSwitchImpl; import org.codehaus.jackson.JsonGenerator; import org.codehaus.jackson.JsonProcessingException; import org.codehaus.jackson.map.JsonSerializer; import org.codehaus.jackson.map.SerializerProvider; import org.openflow.util.HexString; public class OFSwitchImplJSONSerializer extends JsonSerializer<OFSwitchImpl> { /** * Handles serialization for OFSwitchImpl */ @Override public void serialize(OFSwitchImpl switchImpl, JsonGenerator jGen, SerializerProvider arg2) throws IOException, JsonProcessingException { jGen.writeStartObject(); jGen.writeStringField("dpid", HexString.toHexString(switchImpl.getId())); jGen.writeEndObject(); } /** * Tells SimpleModule that we are the serializer for OFSwitchImpl */ @Override public Class<OFSwitchImpl> handledType() { return OFSwitchImpl.class; } }
Now we have to tell Jackson to use the serializer. We do this by annotating our class which will tell Jackson to use our serializer. Open up OFSwitchImpl and annotate the class as of so.
@JsonSerialize(using=OFSwitchImplJSONSerializer.class) public class OFSwitchImpl extends OFSwitchBase implements IOFSwitch {
Loading the module
We're almost done, we just need to tell Floodlight to load the module on startup. First we have to tell the loader that the module exists. This is done by adding the fully qualified module name on it's own line in src/main/resources/META-INF/services/net.floodlight.core.module.IFloodlightModule. We open that file and append this line.
net.floodlightcontroller.pktinhistory.PktInHistory
Then we tell the module to be loaded. We modify the Floodlight module configuration file. The default one is src/main/resources/floodlightdefault.properties. The key is floodlight.modules and the value is a comma separated list of fully qualified module names. After we add PktInHistory it will look something like this.
floodlight.modules = net.floodlightcontroller.staticflowentry.StaticFlowEntryPusher,\ net.floodlightcontroller.forwarding.Forwarding,\ net.floodlightcontroller.pktinhistory.PktInHistory
|
This is just a sample file configuration file. As Floodlight is under rapid development this file may not reflect the most current configuration required to run. Instead of using this configuration exactly just append the PktInHistory module to the current list in the configuration file. |
Testing with mininet
We can also try testing the whole controller by connecting mininet to it. Run the controller as before, then from inside a controller vm:
$ sudo mn --controller=remote --ip=[Your IP Address] --mac --topo=tree,2 *** Adding controller *** Creating network *** Adding hosts: h1 h2 h3 h4 *** Adding switches: s5 s6 s7 *** Adding links: (h1, s6) (h2, s6) (h3, s7) (h4, s7) (s5, s6) (s5, s7) *** Configuring hosts h1 h2 h3 h4 *** Starting controller *** Starting 3 switches s5 s6 s7 *** Starting CLI:
At the mininet prompt, run:
mininet> pingall *** Ping: testing ping reachability h1 -> h2 h3 h4 h2 -> h1 h3 h4 h3 -> h1 h2 h4 h4 -> h1 h2 h3 *** Results: 0% dropped (0/12 lost)
Now we will hit the REST URL and get our results.
$ curl -s http://localhost:8080/wm/pktinhistory/history/json | python -mjson.tool [ { "message": { "bufferId": 256, "inPort": 2, "length": 96, "lengthU": 96, "packetData": "MzP/Uk+PLoqIUk+Pht1gAAAAABg6/wAAAAAAAAAAAAAAAAAAAAD/AgAAAAAAAAAAAAH/Uk+PhwAo2gAAAAD+gAAAAAAAACyKiP/+Uk+P", "reason": "NO_MATCH", "totalLength": 78, "type": "PACKET_IN", "version": 1, "xid": 0 }, "switch": { "dpid": "00:00:00:00:00:00:00:06" } }, { "message": { "bufferId": 260, "inPort": 1, "length": 96, "lengthU": 96, "packetData": "MzP/Uk+PLoqIUk+Pht1gAAAAABg6/wAAAAAAAAAAAAAAAAAAAAD/AgAAAAAAAAAAAAH/Uk+PhwAo2gAAAAD+gAAAAAAAACyKiP/+Uk+P", "reason": "NO_MATCH", "totalLength": 78, "type": "PACKET_IN", "version": 1, "xid": 0 }, "switch": { "dpid": "00:00:00:00:00:00:00:05" } }, etc....etc....