From OpenFlow Wiki
OFTest is a Python based OpenFlow switch test framework and collection of test cases. It is based on unittest which is included in the standard Python distribution.
This document is meant to provide an introduction to the framework, discuss the basics of running tests and to provide examples of how to add tests.
The following documents provide additional background on OFTest.
- The latest information about OFTest on OpenFlow Hub
- OFReferenceTesting: Background on the development of OFTest.
- OFTestReadme: The "getting started" document distributed with OFTest.
- OFTestListPage: Inventory of tests written and to be written for the framework.
- Full Doxygen Python documentation
It is assumed you're familiar with Python and specifically the unittest module (or at least are willing to refer to the doc at Python unittest doc). It's actually pretty straight forward:
- You inherit from unittest.TestCase or a subclass;
- The methods setUp/tearDown are run prior to/after the core function; normally you won't need to redefine these;
- The method runTest provides the core test functionality;
- You call assertTrue, assertEqual to check test conditions.
Familiarity with the OpenFlow Switching protocol and switch/controller setup and interaction is also assumed.
Here's an overview of the OFTest system architecture.
- The test host surrounds the switch under test (SUT) in the above picture.
- The Python OFTest code is run on the test host.
- The SUT has a control plane connection and a set of dataplane connections.
- The controller interface and dataplane interface of OFTest on the test host connect to the SUT connections.
- The controller and dataplane components have Python objects representing them in the OFTest framework.
- The OFTest dataplane object provides methods to send and receive packets (by polling or callback) from the dataplane. It controls the OS interfaces that are connected to the data ports of the SUT.
- The controller object provides methods to exchange OpenFlow messages with the SUT.
The OFTest framework provides a full object representation of OpenFlow messages and the abstractions from the OpenFlow protocol with which to build test scripts.
Basics of unittest
- Inheritance: SimpleProtocol (see below) inherits from unittest.TestCase. It is the basis for all test cases currently provided in OFTest.
- runTest: This is the main routine for a test case.
- Assertions: Use the routines assertTrue, assertEqual, etc., to verify conditions which, if failed, constitute a test case failure.
- setUp: This routine is automatically called by the test framework prior to calling runTest. If you redefine it, make sure to invoke the parent's instance.
- tearDown: This routine is automatically called by the test framework after running runTest. If you redefine it, make sure to invoke the parent's instance.
At this point, you might want to jump down to the Examples below.
Key Class Hierarchies and Components
Once installed (see OFTestReadme) the components of OFTest are available via import of submodules of oftest. Typically, new tests will be written as subclasses of either the basic protocol test, SimpleProtocol, (for tests that require only communication with the switch over the controller interface) or the basic dataplane test, SimpleDataPlane which provides the dataplane object to send and receive packets to/from OpenFlow ports.
SimpleProtocol and SimpleDataPlane are defined in tests/basic.py so normally that file is imported into your test file.
The essential object provided by inheritance from SimpleProtocol (and from SimpleDataPlane which is a subclass of SimpleProtocol) is self.controller. The setUp routine ensures a connection has been made to the SUT prior to returning. Thus, in runTest you can send messages across the control interface simply by invoking methods of this control object. These may be sent unacknowledged or done as transactions (which are based on the XID in the OpenFlow header):
import basic class MyTest(basic.SimpleProtocol): ... # Inside your runTest: rv = self.controller.message_send(msg) # Unacknowledged response, pkt = self.controller.transact(request) # Transaction based on XID
As mentioned, SimpleDataPlane inherits from SimpleProtocol, so you get the controller object as well as the dataplane object, self.dataplane. Sending packets into the switch is done with the send member:
class MyDPTest(basic.SimpleDataPlane): ... # Inside your runTest: pkt = simple_tcp_packet() self.dataplane.send(port, str(pkt))
Packets can be received in the following ways:
- Non-blocking poll:
(port, pkt, timestamp) = self.dataplane.poll()
- Blocking poll:
(port, pkt, timestamp) = self.dataplane.poll(timeout=1)
- Register a handler: Not Yet Implemented.
For the calls to poll, you may specify a port number in which case only packets received on that port will be returned.
(port, pkt, timestamp) = self.dataplane.poll(port_number=2, timeout=1)
The OpenFlow protocol is represented by a collection of objects inside the oftest.message module. In general, each message has its own class. All messages have a header member and data members specific to the message. Certain variable length data is treated specially and is described (TBD).
Here are some examples:
import oftest.message as message ... request = message.echo_request()
request is now an echo_request object and can be sent via self.controller.transact for example.
msg = message.packet_out()
msg is now a packet_out object. msg.data holds the variable length packet data.
msg.data = str(some_packet_data)
This brings us to one of the important variable length data members, the action list. Each action type has its own class. The action list is also a class with an add method which takes an action object.
import oftest.action as action ... act = action.action_output() # Create a new output action object act.port = egress_port # Set the action's parameter(s) msg.actions.add(act) # The packet out message has an action list member
Another key data class is the match object. TBD: Fill this out.
TBD: Add information about stats objects.
OFTest uses Scapy for managing packet data, http://www.secdev.org/projects/scapy/, although you may not need to use it directly. In the example below, we use the function simple_tcp_packet from testutils to generate a packet. The the parse function packet_to_flow_match is called to generate a flow match based on the packet.
from testutils import * import oftest.parse as parse import oftest.cstruct as ofp ... pkt = simple_tcp_packet() match = parse.packet_to_flow_match(pkt) match.wildcards &= ~ofp.OFPFW_IN_PORT
This introduces the low level module oftest.cstruct, here referred to as ofp. This provides the base definitions from which OpenFlow messages are inherited and basic OpenFlow defines such as OFPFW_IN_PORT. Most enums defined in openflow.h are available in this module.
Occasionally, it is convenient to be able to send a packet into a switch without connecting to the controller. The DataPlaneOnly class is the parent that allows you to do this. It instantiates a dataplane, but not a controller object. The classes PacketOnly and PacketOnlyTagged inherit from DataPlaneOnly and send packets into the switch.
Let's look at some example test cases.
The Echo test case is very simple. It creates an Echo message object, tells the controller to run a transaction based on that message and then verifies that the proper response is received. This only requires the controller interface (not the dataplane interface) so we inherit from the SimpleProtocol test case.
|class Echo(SimpleProtocol):||Inherit from SimpleProtocol; gives controller object|
|"""Test echo response with no data"""||Short documentation string for --list option||def runTest(self):||The core test function||request = message.echo_request()||Create an echo request message object|| response, pkt = \|
|Tell controller to run a transaction; send pkt, get response||self.assertEqual(response.header.type, \||Verify that the response was an echo reply||ofp.OFPT_ECHO_REPLY, 'Rsp not echo_reply')|| self.assertEqual(request.header.xid, \|
response.header.xid, 'Bad XID')
|Verify that the transaction ID is correct|| self.assertEqual(len(response.data), 0,\|
|Verify that there was no data in the response|
For the packet out test, we generate a packet out message, put some data as the target egress packet and send the message to the controller. We then query the data ports as we expect a packet to egress from the switch. This is done for each OpenFlow port in the configuration.
|class PacketOut(SimpleDataPlane):||Inherit from SimpleDataPlane to get controller and dataplane objects|
|"""Test packet out function"""||Short description for --list option|
| rc = delete_all_flows(self.controller, basic_logger)|
self.assertEqual(rc, 0, "Failed to delete all flows")
|Clear the flow table and verify success|
|outpkt = simple_tcp_packet()||Create a default TCP packet; we don't really care about the content|
|of_ports = basic_port_map.keys()||Grab the set of ports in the system from our local (basic) config|
|of_ports.sort()||Sort the ports|
|for dp_port in of_ports:||Run the test for each port in the system|
|msg = message.packet_out()||Generate a packet out message|
|msg.data = str(outpkt)||Set the content of the outgoing packet|
|act = action.action_output()||Create an output action object|
|act.port = dp_port||Set the outgoing port in the action|
| self.assertTrue(msg.actions.add(act), \|
'Could not add action to msg')
|Add the action to the message and check success|
|basic_logger.info("PacketOut to: " + str(dp_port))||Remark on operation in the log file|
| rv = self.controller.message_send(msg)|
self.assertTrue(rv == 0, "Error sending out message")
|Send the packet out message to the switch via the control channel and verify success|
| (of_port, pkt, pkt_time) = \|
|Poll for a packet on any data plane port|
|self.assertTrue(pkt is not None, 'Packet not received')||Check packet was received|
|basic_logger.info("PacketOut: got pkt from " + str(of_port))||Remark on packet in log file|
| if of_port is not None:|
self.assertEqual(of_port, dp_port, \
"Unexpected receive port")
|Verify port is as expected|
| self.assertEqual(str(outpkt), str(pkt), \|
'Response packet does not match send packet')
|Verify packet contents were what was placed in the packet out message|
- Give example(s) that require looking up OF message parameters; show the ways to lookup how OF message parameters are accessed and manipulated?
- Some discussion of the flow mod structure including matching and actions
- Then discuss the different ways to validate a test has worked (flow stats, pkts from the dataplane)
The file oftest/tests/testutils.py provides a good number of functions related to creating packets and flow messages. Here is a summary.
- To do:* Organize this section.
This function generates a TCP scapy packet. Default values are specified for the following fields:
dl_dst='00:01:02:03:04:05' ip_src='192.168.0.1' dl_src='00:06:07:08:09:0a' ip_dst='192.168.0.2' dl_vlan_enable=False ip_tos=0 dl_vlan=0 tcp_sport=1234 dl_vlan_pcp=0 tcp_dport=80 dl_vlan_cfi=0 pktlen=100
This routine takes a set of packet fields to modify. It creates three things:
- The ingress packet to send to the switch
- The egress packet expected to exit the switch
- The set of actions to be added to the flow mod request
Default values are coded into the routine, though you can pass in your own dictionaries for these. They must use the parameters names of the function simple_tcp_packet as keys (see above).
This routine runs a typical "set up flow mod, send packet, inspect received packet". You may set up the packet and actions for the flow mod using pkt_action_setup above. A port map must be passed in and all port pairs will be tested (although a maximum number of port-pair test runs may also be specified).
Clear the flow table. May not affect switch configuration that was put in place another configuration mechanism.
Delete all flows and set the port state of all ports no flood.
Check that an expected packet was or was not received at port sets.
Passing Parameters to Tests
There is a facility for passing test-specific parameters into tests that works as follows. On the command line, give the parameter
You can test for these parameters by importing testutils.py and using the function:
my_key1 = testutils.test_param_get(self.config, 'key1')
The config parameter is the global configuration structure copied into the base tests cases (and usually available in each test file). The routine returns None if the key was not assigned in the command line; otherwise it returns the value assigned (17 in this example).
Note that any test may look at these parameters, so use some care in choosing your parameter keys. Currently the keys used are in pktact.py and control whether VLAN tagged packets are used and whether VLAN tag stripping should be included as an action. These parameters include:
- vid=N: Use tagged packets with VLAN id of N
- strip_vlan=bool: If True, add the strip VLAN tag action to the packet test
Other Test Conventions
- Logger object
- The config object (pointer) is assigned to the base test cases in general and can be referenced as self.config. Originally, each file containing tests kept a copy of the config object, but using self.config is currently preferred.
- Configuration object and how it's updated; used as a means of communicating platform specific parameters to test cases
- Port map
- Test utilties
- receive_pkt_check Check for proper receipt/non-receipt of packets
- Integrating test cases into the framework
- Document string
- Init function
Normally, all debug output goes to the file oft.log in the current directory. You can force the output to appear on the terminal (and set the most verbose debug level) with these parameters added to the oft command: