How to add a REST API to a Module

This tutorial assumes a good understanding of how to write Floodlight modules.

Table of Contents

What is a REST API?

A REST API (or REpresentational State Transfer Application Programming Interface) is a mechanism by which a program can exchange data with an external entity over HTTP at runtime. The basic premise is that an application opens a network server socket and listens for HTTP requests. External users or even other programs can access this API and send and receive messages to and from it. Depending on the HTTP request received, or more specifically the URI and the HTTP command, the application can perform some prescribed task.

Floodlight implements a REST API in the controller core, but there are also many implemented within application modules. For example, the Static Entry Pusher module allows a user to send flows to Floodlight, as well as delete and query for flows using a HTTP request to published URIs. (More information on the Static Entry Pusher REST API is available here.) Other Floodlight modules that implement REST APIs include, but are not limited to, the Device Manager, the Firewall, and the Topology Manager. Using these REST APIs, one can ask Floodlight for the devices learned on the network, to install a firewall rule blocking a device, or to compute a route to one device from another, respectively.

Creating a REST API

You too can implement a REST API for your own module. This will give you the ability to interact with your module while it's running using the APIs you define. It might seem like a daunting task to create such a web-accessible interface, but the process is made quite easy through the IRestApiService Floodlight service. This service is backed by Restlet, which provides the underlying HTTP server and the processing and routing of HTTP requests to handlers within Floodlight.

What does this mean to us as Floodlight module writers? We can leverage the IRestApiService to register our module's REST API defined with the URIs we create. The IRestApiService will in turn handle the mid-level details of configuring the API with Restlet, while Restlet itself abstracts away all the messy low-level details of the web server itself.

To help demonstrate the process of creating a REST API in Floodlight, we will examine the Static Entry Pusher in close detail. We will conduct a stepwise discussion of how the Static Entry Pusher implements its REST APIs for adding, removing, and querying for flows. Note that although we are going to use the Static Entry Pusher as a vehicle for this discussion, the same applies to all Floodlight modules that implement a REST API, and this guide can be used to assist in the creation of our very own REST API in a custom module.

Links to Static Entry Pusher code are provided throughout this tutorial to the master branch implementation. Excerpts from these links are provided to facilitate talking points.


 On a high-level, there are four steps to implementing a REST API in Floodlight: 

  1. Get the IRestApiService
  2. Define the URIs we want for our module
  3. Implement the URIs defined in our module
  4. Tell Floodlight our new REST API exists

Let's walk through each of these steps one at a time.

Get the IRestApiService

The first step in creating a REST API for a module involves the IRestApiService. The IRestApiService Floodlight service provides a REST API registration mechanism for all modules. Each module that implements a REST API simply registers the API with the IRestApiService, and the IRestApiService takes it from there handling the low-level details for us.

To do this, we need to depend on the IRestApiService in our module to guarantee the service is loaded prior to the time our module's init() function is invoked. This can be done using IFloodlightModule's getModuleDependencies() function.

@Override
public Collection<Class<? extends IFloodlightService>> getModuleDependencies() {
	Collection<Class<? extends IFloodlightService>> l =
			new ArrayList<Class<? extends IFloodlightService>>();
	...
	...
	l.add(IRestApiService.class);
	return l;
}

The next thing to do is obtain a reference to the IRestApiService. This can be done in the same manner as for IFloodlightProviderService and IOFSwitchService as shown below in IFloodlightModule's init().

@Override
public void init(FloodlightModuleContext context) throws FloodlightModuleException {
	...
	...
	restApiService = context.getServiceImpl(IRestApiService.class);
	...
	...
} 

Define the URIs We Want for Our Module

A very important part in implementing a REST API is defining the actual REST interfaces we want to expose. Only these URIs will be available for use. If we don't define the URI here, then it will not be possible for Restlet to route the request to us.

The first step is to create a class to implement Restlet's RestletRoutable interface, overriding basePath() and getRestlet() as shown here and below.

public class StaticEntryWebRoutable implements RestletRoutable {
    /**
     * Create the Restlet router and bind to the proper resources.
     */
    @Override
    public Restlet getRestlet(Context context) {
        Router router = new Router(context);
        router.attach("/json", StaticEntryPusherResource.class);
        router.attach("/json/store", StaticEntryPusherResource.class);
        router.attach("/json/delete", StaticEntryDeleteResource.class);
        router.attach("/clear/{switch}/json", ClearStaticEntriesResource.class);
        router.attach("/list/{switch}/json", ListStaticEntriesResource.class);
        return router;
    }

    /**
     * Set the base path for the Topology
     */
    @Override
    public String basePath() {
        return "/wm/staticentrypusher";
    }
}

We will provide the IRestApiService with this class when we register later on. All we have to do is define our base path and the specific URIs we want to expose in the basePath() and getRestlet() functions, respectively.

Note the use of {switch} in the Static Entry Pusher's clear and list APIs. This is how we can wildcard part of our URI. {switch} is a variable that can be accessed when the API is called. It'll be set to whatever the person/program calling the API uses in place of the variable in the URI. In the case of the Static Entry Pusher, it's used to specify a switch DPID or the "all" keyword for all DPIDs.

Implement the URIs Defined in Our Module

Next, we need to define the classes that will implement each URI defined in our getRestlet() function shown above. In the case of the Static Entry Pusher, this is StaticEntryPusherResource, StaticEntryDeleteResource, ClearStaticEntriesResource, and ListStaticEntriesResource. These are what will handle a request to each of the URIs defined.

So, let's take a look at one as an example -- ListStaticEntriesResource. This and all of these URI-handler classes must extend ServerResource and have whatever functions we define in them. We do not need to override any functions or name any functions in a special way. However, we must annotate functions with the HTTP command they are to process. We can annotate with @Get, @Post, @Put, @Delete for the common HTTP commands (there are others though). @Get is demonstrated in our ListStaticEntriesResource example here and below.

public class ListStaticEntriesResource extends ServerResource {
    protected static Logger log = LoggerFactory.getLogger(ListStaticEntriesResource.class);
    
    @Get("json")
    public SFPEntryMap ListStaticEntries() {
        IStaticEntryPusherService sfpService =
                (IStaticEntryPusherService)getContext().getAttributes().
                    get(IStaticEntryPusherService.class.getCanonicalName());

        String param = (String) getRequestAttributes().get("switch");
        if (log.isDebugEnabled())
            log.debug("Listing all static entires for switch: " + param);
        if (param.toLowerCase().equals("all")) {
            return new SFPEntryMap(sfpService.getFlows());
        } else {
            try {
                Map<String, Map<String, OFMessage>> retMap = new HashMap<String, Map<String, OFMessage>>();
                retMap.put(param, sfpService.getEntries(DatapathId.of(param)));
                return new SFPEntryMap(retMap);
            } catch (NumberFormatException e){
                setStatus(Status.CLIENT_ERROR_BAD_REQUEST, ControllerSwitchesResource.DPID_ERROR);
            }
        }
        return null;
    }
}

The @Get annotation will tell the compiler to link this function, ListStaticEntries(), with any HTTP GET messages sent to the URI /wm/staticentrypusher/list/{switch}/json. In general, any HTTP command XXX that does not have a corresponding @XXX annotation will not be handled. We can have more than one annotation per function to allow it to handle multiple HTTP commands.

Furthermore, as shown above, we can also, optionally, specify the type of data in any annotation. The @Get example above is actually @Get("json"), which tells Restlet to expect JSON data as output when the function returns. We can use other data formats, XML for example via @Get("xml"). Floodlight, however, uses JSON throughout.

Taking a closer look here, we can see within ListStaticEntries above where the {switch} variable comes into play. Using getRequestAttributes(), we can retrieve the "switch" attribute from the request. It'll be a string, which we can then examine and act upon. We can use any attribute string name we want; "switch" is used in the Static Entry Pusher, since we're using it to determine what switch DPID to examine in the request. We can also have more than one attribute per URI by including multiple curly-bracket-enclosed variables in our URI. This will wildcard those parts of the request when Restlet tries to match the request URI against those registered.

Parsing JSON Input

We can also take advantage of Jackson to help parse any JSON you might provide as input via an HTTP PUT or POST. In the Static Entry Pusher, we take a flow defined in JSON as input here in StaticEntryPusherResource.

@Post
public String store(String fmJson) {
	IStorageSourceService storageSource =
		(IStorageSourceService)getContext().getAttributes().
			get(IStorageSourceService.class.getCanonicalName());
	Map<String, Object> rowValues;
	try {
		rowValues = StaticEntries.jsonToStorageEntry(fmJson);
		...
	}
	...
}

Note also the @Post annotation above to tell the function to be invoked on a post in the first place. It, combined with a String argument to our function is how we can receive the data "post"ed. Now, the most important and tedious part is actually parsing out the JSON and making sense of it in our code. The Static Entry Pusher parses the JSON-defined flow here and in short below in jsonToStorageEntry(), which, again, we can see is called from the function above.

public static Map<String, Object> jsonToStorageEntry(String fmJson) throws IOException {
	Map<String, Object> entry = new HashMap<String, Object>();
	MappingJsonFactory f = new MappingJsonFactory();
	JsonParser jp;
	...
	...
	try {
		jp = f.createJsonParser(fmJson); /* deprecated, replace with f.createParser(fmJson); */
	} catch (JsonParseException e) {
		throw new IOException(e);
	}

	jp.nextToken();
	if (jp.getCurrentToken() != JsonToken.START_OBJECT) {
		throw new IOException("Expected START_OBJECT");
	}

	while (jp.nextToken() != JsonToken.END_OBJECT) {
		if (jp.getCurrentToken() != JsonToken.FIELD_NAME) {
			throw new IOException("Expected FIELD_NAME");
		}
		String n = jp.getCurrentName();

		jp.nextToken();

		switch (n) {
		case StaticEntryPusher.COLUMN_NAME:
			entry.put(StaticEntryPusher.COLUMN_NAME, jp.getText());
			break;
		case StaticEntryPusher.COLUMN_SWITCH:
			entry.put(StaticEntryPusher.COLUMN_SWITCH, jp.getText());
			break;
		case StaticEntryPusher.COLUMN_TABLE_ID:
			entry.put(StaticEntryPusher.COLUMN_TABLE_ID, jp.getText());
			break;
		case StaticEntryPusher.COLUMN_ACTIVE:
			entry.put(StaticEntryPusher.COLUMN_ACTIVE, jp.getText());
			break;
		case StaticEntryPusher.COLUMN_IDLE_TIMEOUT:
			entry.put(StaticEntryPusher.COLUMN_IDLE_TIMEOUT, jp.getText());
			break;
		case StaticEntryPusher.COLUMN_HARD_TIMEOUT:
			entry.put(StaticEntryPusher.COLUMN_HARD_TIMEOUT, jp.getText());
			break;
		...
		...
		}
	...
	...
	return entry;
}

Composing JSON Output

Likewise, we can use Jackson to compose a JSON message to return from our HTTP command handler function. As an output example, here's how the list of entries are converted from Java object form to JSON when the /wm/staticentrypusher/list/{switch}/json API is invoked. A snippit of the code is provided below.

public class ListStaticEntriesResource extends ServerResource {
	...
    
    @Get("json")
    public SFPEntryMap ListStaticEntries() {
        ...
        return new SFPEntryMap(sfpService.getEntries());
        ...
}

As we can see, a new object of SFPEntryMap is returned, which naturally leads us to the class SFPEntryMap. Let's take a close look at the @JsonSerialize annotation used at the top of OFFlowModMap, reproduced below. 

@JsonSerialize(using=SFPEntryMapSerializer.class) 
public class SFPEntryMap {
	/*
	 * Contains the following double-mapping:
	 * Map<Switch-DPID-Str, Map<Entry-Name-Str, OFMessage>>
	 */
	private Map<String, Map<String, OFMessage>> theMap;
	
	public SFPEntryMap (Map<String, Map<String, OFMessage>> theMap) {
		this.theMap = theMap;
	}

	public Map<String, Map<String, OFMessage>> getMap() {
		return theMap;
	}
}

It specifies the class that converts any object of SFPEntryMap type to JSON. This class is SFPEntryMapSerializer, which is located here and also reproduced below.

public class SFPEntryMapSerializer extends JsonSerializer<SFPEntryMap> {
	@Override
	public void serialize(SFPEntryMap em, JsonGenerator jGen, SerializerProvider serializer)
			throws IOException, JsonProcessingException {
        jGen.configure(Feature.WRITE_NUMBERS_AS_STRINGS, true);

		if (em == null) {
			jGen.writeStartObject();
			jGen.writeString("No flows have been added to the Static Entry Pusher.");
			jGen.writeEndObject();
			return;
		}

		Map<String, Map<String, OFMessage>> theMap = em.getMap();

		jGen.writeStartObject();
		if (theMap.keySet() != null) {
			for (String dpid : theMap.keySet()) {
				if (theMap.get(dpid) != null) {
					jGen.writeArrayFieldStart(dpid);
					for (String name : theMap.get(dpid).keySet()) {
						jGen.writeStartObject();
						jGen.writeFieldName(name);
						if (theMap.get(dpid).get(name) instanceof OFFlowMod) {
							OFFlowModSerializer.serializeFlowMod(jGen, (OFFlowMod) theMap.get(dpid).get(name));
						} else if (theMap.get(dpid).get(name) instanceof OFGroupMod) {
							OFGroupModSerializer.serializeGroupMod(jGen, (OFGroupMod) theMap.get(dpid).get(name));
						}
						jGen.writeEndObject();
					}    
					jGen.writeEndArray();
				}
			}
		}
		jGen.writeEndObject();
	}
}

As shown above, we must extend JsonSerializer in our class, or JsonSerializer<SFPEntryMap> in the case of our Static Entry Pusher example. We also must override the serialize() function, which allows our custom JSON serializer to be invoked. Ending this example of how to produce JSON output, we can follow OFFlowModSerializer.serializeFlowMod() to the implementation here and below.

public static void serializeFlowMod(JsonGenerator jGen, OFFlowMod flowMod) throws IOException, JsonProcessingException {
    jGen.configure(Feature.WRITE_NUMBERS_AS_STRINGS, true); // IMHO this just looks nicer and is easier to read if everything is quoted
 
	jGen.writeStartObject();
	jGen.writeStringField("version", flowMod.getVersion().toString()); // return the enum names
	jGen.writeStringField("command", flowMod.getCommand().toString());
	jGen.writeNumberField("cookie", flowMod.getCookie().getValue());
	jGen.writeNumberField("priority", flowMod.getPriority());
	jGen.writeNumberField("idleTimeoutSec", flowMod.getIdleTimeout());
	jGen.writeNumberField("hardTimeoutSec", flowMod.getHardTimeout());
	jGen.writeStringField("outPort", flowMod.getOutPort().toString());
	switch (flowMod.getVersion()) {
	case OF_10:
		break;
	case OF_11:
		jGen.writeNumberField("flags", OFFlowModFlagsSerializerVer11.toWireValue(flowMod.getFlags()));
		jGen.writeNumberField("cookieMask", flowMod.getCookieMask().getValue());
		jGen.writeStringField("outGroup", flowMod.getOutGroup().toString());
		jGen.writeStringField("tableId", flowMod.getTableId().toString());
		break;
	case OF_12:
		jGen.writeNumberField("flags", OFFlowModFlagsSerializerVer12.toWireValue(flowMod.getFlags()));
		jGen.writeNumberField("cookieMask", flowMod.getCookieMask().getValue());
		jGen.writeStringField("outGroup", flowMod.getOutGroup().toString());
		jGen.writeStringField("tableId", flowMod.getTableId().toString());
		break;
	case OF_13:
		jGen.writeNumberField("flags", OFFlowModFlagsSerializerVer13.toWireValue(flowMod.getFlags()));
		jGen.writeNumberField("cookieMask", flowMod.getCookieMask().getValue());
		jGen.writeStringField("outGroup", flowMod.getOutGroup().toString());
		break;
	case OF_14:
		jGen.writeNumberField("flags", OFFlowModFlagsSerializerVer14.toWireValue(flowMod.getFlags()));
		jGen.writeNumberField("cookieMask", flowMod.getCookieMask().getValue());
		jGen.writeStringField("outGroup", flowMod.getOutGroup().toString());
		jGen.writeStringField("tableId", flowMod.getTableId().toString());
		break;
	default:
		logger.error("Could not decode OFVersion {}", flowMod.getVersion());
		break;
	}

	MatchSerializer.serializeMatch(jGen, flowMod.getMatch());
	// handle OF1.1+ instructions with actions within
	if (flowMod.getVersion() == OFVersion.OF_10) {
		jGen.writeObjectFieldStart("actions");
		OFActionListSerializer.serializeActions(jGen, flowMod.getActions());
		jGen.writeEndObject();
	} else {
		OFInstructionListSerializer.serializeInstructionList(jGen, flowMod.getInstructions());
	} // end not-empty instructions (else)
	jGen.writeEndObject();
} // end method

Tell Floodlight Our New REST API Exists

Now, let's come full circle and go back to our module where we obtained our reference to the IRestApiService. In our module, we can register our new REST API with the IRestApiService. This is typically done in our module's startup function as shown here and below.

@Override
public void startUp(FloodlightModuleContext context) {
	...
	...
	restApiService.addRestletRoutable(new StaticEntryWebRoutable());
}

Note that this should not go in init(), since we need IRestApi's startup() function to be called (fully setting up the service). As we can see, an instance of your RestletRoutable implementation is created. The great news is that the IRestApiService and Restlet take it from here. Nothing more to do other than issue commands to our new REST API and enjoy :-)

Questions, comments? Write to our email list.