Entry point of framework is object ModuleManager. Run it to start interactive console, which, in its turn, allows you manage components and start accepting of incoming connections (command start <ip> <port>).
Component is a Java class designed according to several rules. For now, there are two main types of components: [factory of message drivers] MessageDriverFactory and module Module.
The purpose of the first type is providing a set of message drivers, classes that decode incoming messages (messages from switches to controller) and encode outgoing messages (messages from controller to switches). The reason why there is a set of drivers is that controller is able to serve different protocol versions, each of which can be supported by a different driver.
The second type of components can be used for different purposes, from [handling messages from switches] MessageHandlers (it is the main purpose) to executing actions that are not related to SDN at all (that is not a good idea in general). Typically, module reacts on an event occurred in the system via publishing other events.
Commands for components management (in interactive console):
set factory <factory_class_name> -p [<param1>, < param2>, …]- loads factory (reloading is not implemented yet)add module <module_name> of type <module_class_name> -p [<param1>, <param2>, …]– loads modulerm module <module_name>- removes already loaded module
- Classes of factories and modules must be included in class path.
- Loaded factory and modules must be [compatible] Compatibility each with other.
Message driver is a class which implements MessageDriver interface. These classes are actually implementations of OpenFlow protocol. The protocol implemented by a driver could match an opennetworking specification or could implement a proprietary-extended protocol. Message drivers are used by controller to serialize objects representing OF messages to arrays of bytes and deserialize them and to perform procedure of handshake.
trait MessageDriver[T <: OFMessage] is a union of traits, listened below:
trait OfpVersionSupport- provides information about version supported by drivertrait OfpHandshakeSupport- provides methods used in the handshake proceduretrait OfpFeaturesExtractor[-T <: OFMessage]- provides methods which extract values of some common fields from raw data or already deserialized object.trait OfpMessageEncoder[-T <: OFMessage]- serializes object of type T to array of bytestrait OfpMessageDecoder[+T <: OFMessage]- deserializes object of type T from array of bytes
The tree last traits are parametrized over type of messages they deal with. Driver developers define it and it must be a subclass of OFMessage.
One of the main goals of warp is to provide a way of definition of user-defined versions of OpenFlow protocols which can be reloaded without restart of the entire controller and to develop end-user API to operate with such protocols.
Package [org.flowforwarding.warp.driver_api.dynamic] PackageDynamic provides a set of classes to achieve the above goal.
trait DynamicStructure represents messages, structure of which is not defined in compile-time but depends on the loaded driver definition. User is able to get fields of structures using names of that fields.
trait DynamicStructureBuilder[+E <: DynamicStructure] builds dynamic structures from instances of DynamicBuilderInputs and makes DynamicBuilderInputs which user fills.
trait DynamicBuilderInput represents a structure which should be built as its name and a set of named members.
trait DynamicDriver[E <: DynamicStructure] provides functionality of MessageDriver and DynamicStructureBuilder for a specific subtype of DynamicStructure.
Consider an example of these traits usage:
DynamicStructureBuilder driver = // e.g. we got it as a parameter of a method
DynamicStructure incomingMessage = // ...
if(incomingMessage.isTypeOf("ofp_switch_features_reply")){
System.out.println("Connected to: " + incomingMessage.primitiveField("datapath_id"));
DynamicBuilderInput reqHeader =
driver.newBuilderInput("ofp_header")
.setMember("xid", new DynamicPrimitive(XidGenerators.nextXid())))
.setMember("length", new DynamicPrimitive(8L + 4));
DynamicBuilderInput request =
driver.newBuilderInput("echo_request")
.setMember("header", new DynamicStructureInput(reqHeader))
.setMember("elements", new DynamicPrimitives(new long[]{ 2, 2, 2, 2});
DynamicStructure result = driver.build(request).get()
// send result
}- Look through the definition of the used protocol to determine which fields are supported by DynamicStructures or DynamicBuilderInputs. It is impossible and actually unnecessary to have this information in runtime.
- If you have changed and reloaded definition of protocol, modules which use Dynamic API could got broken because of changed names of fields. In this case all the modules which used changed fields should be fixed and reloaded too.
- This API is quite verbose, so controller provides two wrappers (for both [Scala] DynamicScalaApi and [Java] DynamicJavaApi) providing more convenient and language-specific APIs for writing MessageHandlers MessageHandlers.
Warp controller provides class-based APIs for opennetworking-specified versions of OpenFlow protocol (for now for 1.3 only). The advantage of these APIs is type-safety: user operates with compile-time defined structures and handlers, so there is no need to check types of messages and no risk to get a runtime exception because of absence of a required field. Fixed APi is built on top of Dynamic API.
To declare support of Fixed API for version V, dynamic driver should be mixed with a trait corresponding to version V, e. g. to support version 1.3, it should be mixed with Ofp13DriverApi. Such traits require definition of field val namesConfig: Config that shows how fields required by this version of Fixed API are named in the underlying driver.
Besides class-based representation, Fixed API supports textual representation of OFP structures:
- each Structure is convertible to its text view using method
def textView: BITextView - Fixed API Driver is able to parse BuilderInputs using method
def parseTextView(input: BITextView): Try[BuilderInput]
Textual representation was developed for interconnection with out-of-controller sources of messages, e. g. it is used in Send-To-Switch Rest API.
Information about definition of new versions of fixed API you can find in corresponding guide.
The current implementation of Fixed API expects that underlying DynamicDriver is able to determine values of fields based on values of other fields or type of structure (like fields length and type of structure ofp_header).
Each driver instance supports own version of protocol, but controller is able to serve switches which work with different protocol versions. That is the reason why trait MessageDriverFactory exists: when switch connects to controller, controller asks the loaded factory about the highest common version between versions of drivers it provides and versions supported by the switch. Then this version is used to get a driver which will serve this concrete switch.
Factories of Message Drivers are loadable components of warp framework. It should be the first component loaded into controller because some modules may have specific requirements to factories (support of a particular version of Fixed API or common message type, etc) and these requirements must be tested while module loading.
Although further goal of warp is protocol-independence, for now message drivers and factories are designed to support only OpenFlow protocols.
For now there are three implementations of MessageDriver:
Event bus in warp is implemented in terms of actors and messages. It is the main way of communication between modules and place where messages from switches emerge, therefore, every module [holds a reference] ModuleDev to a needed message bus.
There are two types of buses: MessageBus and ServiceBus. MessageBus provides a way of subscription and publishing messages (fire-and-forget model), whereas ServiceBus registers services and allows other modules turn to them for information (request-reply model). ControllerBus unites both ServiceBus and MessageBus; it is created once during initialization of framework and shared between all the modules, so that there is only one EventBus for now.
To grant an actor access to an event bus, mixin trait ServiceBusActor, MessageBusActor or ControllerBusActor to your actor class.
MessageBusActor provides the following methods:
val bus: MessageBus
final def publishMessage(msg: MessageEnvelope)
final def subscribe(subscriptionName: String)(subscriptionPredicate: PartialFunction[MessageEnvelope, Boolean]): Unit
final def subscribe(subscriptionName: String, subscriptionPredicate: MessageEnvelopePredicate): Unit /* Java API */
final def unsubscribe(subscriptionName: String): Unit
final def unsubscribe(): UnitpublishMessage publishes a message on the message bus.
Method subscribe defines which messages actor would receive. Note that there could be several subscriptions, each of which can be dynamically removed using string-identifier of subscription and method unsubscribe(subscriptionName: String) or remove them all using unsubscribe().
ServiceBusActor provides the following methods:
val bus: ServiceBus
final def askService(request: ServiceRequest): Future[Any]
final def registerService(acceptedRequests: PartialFunction[ServiceRequest, Boolean])
final def unregisterService(): UnitaskService publishes request on the bus and returns a Future which is successful and holds response if a service was able to handle request and it is failed if service was not found or failed to handle request.
Actors use registerService to declare self as a service and define which types of requests they are able to handle and unregisterService to remove self from the list of services.
trait MessagesSender[M <: OFMessage] is successor of ServiceBusActor and provides methods for sending messages of type M to switches. It depends on service provided by special internal modules - SwitchConnectors SwitchConnectors.
trait FixedStructuresSender simplifies send routines for FixedApi: it deals with BuilderInputs and TextViews and assumes that message driver supports Fixed API.
Method bus of BusActors is abstract and is typically implemented as a parameter of constructor of class implementing it (in Scala).
ServiceRequest and MessageEnvelope are the basic traits for each request and message, respectively. Their successors are usually defined in a companion object of actor which introduces this kind of message to the system.
"Actor introduces" means that this kind of message is not already defined and
- actor is a service which serves this kind of requests, or
- actor fires such messages, or
- actor reacts on this kind of message.
Module is the basic abstraction for component of warp controller framework. Since Warp is built on top of Akka, it is naturally to represent each module as an Actor, so trait Module extends Actor and provides additional methods
protected def started(): Unit
protected def shutdown(): Unit
protected def compatibleWith(factory: MessageDriverFactory[_]): BooleanMethod started is a good place for initialization of module (allocate resources, subscribe to events, register services etc.), shutdown should hold logic of finalization, and compatibleWith tests is this module compatible with the loaded driver factory.
As mentioned above, components are represented by Java classes. In case of modules, class should be successor of trait Module.
To develop a module loadable by module manager, user should:
- Inherit from class Module or its successor.
- Provide constructor which takes
XxxBusas the first parameter and strings as others (variable-size argument is allowed too).
Note that although user can inherit from trait Module directly and mix it with MessageBusActor or ServiceBusActor traits to make the module be able to communicate with other modules (see predefined module RestApiServer), typically it is not necessary, because of two traits-successors of Module – MessageConsumer and Service.
MessageConsumer is the basic trait for all modules which handle events (like MessageHandlers). It mixes Module with MessageBusActor and provides a method
protected def handleEvent(e: MessageEnvelope): Unitwhich should be overridden in successors in order to handle messages on which this Module is subscribed. It is supposed that MessageConsumer subscribes for messages overriding method started and\or as reaction on an event.
Service is the basic trait for all modules providing services. It mixes Module with ServiceBusActor and provides a method
protected def handleRequest(e: ServiceRequest): Future[Any]which should be overridden in successors in order to handle requests to a service which this Module provides. It is supposed that Service declares service it provides in overridden method started.
As an example of service implementation, consider
- RestApiService, a template for all Rest-Api services
- and its successor, a predefined service SendToSwitchService
Switch connectors are special kind of modules. They are not supposed to be loaded by user: ModuleManager creates a SwitchConnector when accepts new connection with SDN switch. Once SwitchConnector is created, it handles networking aspect of controller-switch interaction: performs handshake procedure, sends incoming/outgoing messages, etc. It acknowledges other modules about occurring events via publishing messages listened below:
SwitchHandshake[T <: OFMessage](dpid: ULong, driver: MessageDriver[T])- notifies about connection establishmentSwitchDisconnected[T <: OFMessage](dpid: ULong, driver: MessageDriver[T])- notifies about connection closingSwitchIncomingMessage[T <: OFMessage](id: ULong, driver: MessageDriver[T], msg: T)- notifies about incoming message from switch (both asynchronous and reply to a request)
On the other hand, any SwitchConnector is a service. It serves two types of requests:
SendToSwitch[T <: OFMessage](id: ULong, msg: T, needReply: Boolean)- if ID of serviced switch equals id, sends message msg to this switch and replies with SendingResult SendingResult to request sender if needReply = true.ForceDisconnect(dpid: ULong)- closes connection if ID of serviced switch equals dpid.
For now SwitchConenctors support only OpenFlow switches. It is planned to make SwitchConnectors protocol-independent.
Sometimes modules require concrete requirements to driver (e.g. it must be a DynamicDriver), supported versions of protocol etc. Specify this contract in module's method compatibleWith, which is called while controller loads module to make sure it will work correct. Note that this method takes MessageDriverFactory as a parameter, thus it is possible to check availability of drivers with required properties.
Message handler is a module which reacts on messages from switches. Every message handler is a subtype of
abstract class MessageHandlers[T <: OFMessage, ApiSupport <: OfpVersionSupport]. This class is parametrized over subtype of OFPMessage which it is able to handle (type T) and API, typically used to create response or extract information from incoming message (type ApiSupport). MessageHandlers mixes trait MessageConsumer and trait ServiceBusActor and provides additional methods
def supportedVersions: Array[UByte]
def handleMessage(api: ApiSupport, dpid: ULong, msg: T): Try[Array[T]]
def handleDisconnected(api: ApiSupport, dpid: ULong): Unit
def handleHandshake(api: ApiSupport, dpid: ULong): UnitMethod supportedVersions declares versions of OpenFlow protocol this module is able to handle, methods handleHandshake and handleDisconnected define reaction on connection and disconnection of a switch, and handleMessage defines response (a sequence of outgoing messages) to an incoming message.
Each message handler is an actor, it has local state and own messages queue, so it may become a bottleneck. This problem can be solved using programmatic loading of several different handlers of the same type (send service request ModuleManager.AddModule) and distribution of messages between them (override method started and there subscribe to a messages filtered by id of switch, for example).
There are several successors of class MessageHandlers, providing more convenient usage of Dynamic API, Fixed APIs and driver-specific APIs. Those of them which were designed for a specific implementation of API, locate in corresponding <specific-implementation-name>-adapter projects.
Project OpenDaylight provides set of Rest APIs for switch management and network state monitoring. Warp implements some of them as part of controller, so many applications which use these API could be used together with warp.
The following APIs are implemented:
Controller provides modules implementing protocol-independent [abstractions] AbstractRestServices, which require modules which provide concrete underlying implementations. These modules should be loaded into controller together with basic modules and RestApiServer RestApiServer. You could find them in corresponding <specific-implementation-name>-adapter projects. For now they exist (but Topology) only for [Fixed OpenFlow 1.3 API] FixedOFP13RestServices, project driver-api-ofp13-adapter.
For example, if you want to add SwitchManager Rest API and its OpenFlow implementation, paste this code into interactive console:
add module rest_api_server of type org.flowforwarding.warp.controller.modules.rest.RestApiServer -p /controller/nb/v2
add module sm_nb of type org.flowforwarding.warp.controller.modules.opendaylight_rest.northbound.SwitchManagerNorthbound -p /controller/nb/v2
add module inventory_manager of type org.flowforwarding.warp.controller.modules.opendaylight_rest.managers.InventoryManager -p
add module ofp_inventory of type org.flowforwarding.warp.controller.modules.opendaylight_rest.impl.openflow.v13.Ofp13InventoryService -p
More information about development of modules supporting Rest API for your driver you can find [here] RestServicesDev.
- For now such features of OpenDaylight Rest API as Authorization and Containers are not implemented and only json-based messages are supported.
- Be aware that Rest API modules may depend on each other.