blob: c4b14919f344bcc2564f76d538b5fe83b27ea581 [file] [log] [blame]
Dan Talayco48370102010-03-03 15:17:33 -08001#!/usr/bin/env python
2"""
3@package oft
4
5OpenFlow test framework top level script
6
7This script is the entry point for running OpenFlow tests
8using the OFT framework.
9
10The global configuration is passed around in a dictionary
Brandon Heller88f709d2010-04-01 12:29:56 -070011generally called config. The keys have the following
Dan Talayco48370102010-03-03 15:17:33 -080012significance.
13
Dan Talayco2c0dba32010-03-06 22:47:06 -080014<pre>
Dan Talayco48370102010-03-03 15:17:33 -080015 platform : String identifying the target platform
16 controller_host : Host on which test controller is running (for sockets)
17 controller_port : Port on which test controller listens for switch cxn
18 port_count : (Optional) Number of ports in dataplane
19 base_of_port : (Optional) Base OpenFlow port number in dataplane
20 base_if_index : (Optional) Base OS network interface for dataplane
Dan Talayco2c0dba32010-03-06 22:47:06 -080021 test_dir : (TBD) Directory to search for test files (default .)
Dan Talayco48370102010-03-03 15:17:33 -080022 test_spec : (TBD) Specification of test(s) to run
23 log_file : Filename for test logging
Dan Talayco2c0dba32010-03-06 22:47:06 -080024 list : Boolean: List all tests and exit
Dan Talayco48370102010-03-03 15:17:33 -080025 debug : String giving debug level (info, warning, error...)
Dan Talayco2c0dba32010-03-06 22:47:06 -080026</pre>
Dan Talayco48370102010-03-03 15:17:33 -080027
28See config_defaults below for the default values.
29
Dan Talayco2c0dba32010-03-06 22:47:06 -080030The following are stored in the config dictionary, but are not currently
31configurable through the command line.
32
33<pre>
34 dbg_level : logging module value of debug level
35 port_map : Map of dataplane OpenFlow port to OS interface names
36 test_mod_map : Dictionary indexed by module names and whose value
37 is the module reference
38 all_tests : Dictionary indexed by module reference and whose
39 value is a list of functions in that module
40</pre>
41
Dan Talaycoc24aaae2010-07-08 14:05:24 -070042Each test may be assigned a priority by setting test_prio["TestName"] in
43the respective module. For now, the only use of this is to avoid
44automatic inclusion of tests into the default list. This is done by
45setting the test_prio value less than 0. Eventually we may add ordering
46of test execution by test priority.
47
Dan Talayco2c0dba32010-03-06 22:47:06 -080048To add a test to the system, either: edit an existing test case file (like
49basic.py) to add a test class which inherits from unittest.TestCase (directly
50or indirectly); or add a new file which includes a function definition
51test_set_init(config). Preferably the file is in the same directory as existing
52tests, though you can specify the directory on the command line. The file
53should not be called "all" as that's reserved for the test-spec.
54
55If you add a new file, the test_set_init function should record the port
56map object from the configuration along with whatever other configuration
57information it may need.
58
59TBD: To add configuration to the system, first add an entry to config_default
Dan Talayco48370102010-03-03 15:17:33 -080060below. If you want this to be a command line parameter, edit config_setup
61to add the option and default value to the parser. Then edit config_get
62to make sure the option value gets copied into the configuration
63structure (which then gets passed to everyone else).
64
65By convention, oft attempts to import the contents of a file by the
66name of $platform.py into the local namespace.
67
68IMPORTANT: That file should define a function platform_config_update which
69takes a configuration dictionary as an argument and updates it for the
70current run. In particular, it should set up config["port_map"] with
71the proper map from OF port numbers to OF interface names.
72
73You can add your own platform, say gp104, by adding a file gp104.py
74that defines the function platform_config_update and then use the
75parameter --platform=gp104 on the command line.
76
77If platform_config_update does not set config["port_map"], an attempt
78is made to generate a default map via the function default_port_map_setup.
79This will use "local" and "remote" for platform names if available
80and generate a sequential map based on the values of base_of_port and
81base_if_index in the configuration structure.
82
Dan Talayco48370102010-03-03 15:17:33 -080083The current model for test sets is basic.py. The current convention is
84that the test set should implement a function test_set_init which takes
85an oft configuration dictionary and returns a unittest.TestSuite object.
86Future test sets should do the same thing.
87
Dan Talayco52f64442010-03-03 15:32:41 -080088Default setup:
89
90The default setup runs locally using veth pairs. To exercise this,
91checkout and build an openflow userspace datapath. Then start it on
92the local host:
Dan Talayco2c0dba32010-03-06 22:47:06 -080093<pre>
Dan Talayco52f64442010-03-03 15:32:41 -080094 sudo ~/openflow/regress/bin/veth_setup.pl
95 sudo ofdatapath -i veth0,veth2,veth4,veth6 punix:/tmp/ofd &
96 sudo ofprotocol unix:/tmp/ofd tcp:127.0.0.1 --fail=closed --max-backoff=1 &
97
98Next, run oft:
99 sudo ./oft --debug=info
Dan Talayco2c0dba32010-03-06 22:47:06 -0800100</pre>
Dan Talayco52f64442010-03-03 15:32:41 -0800101
102Examine oft.log if things don't work.
Dan Talayco2c0dba32010-03-06 22:47:06 -0800103
Dan Talayco1a88c122010-03-07 22:00:20 -0800104@todo Support per-component debug levels (esp controller vs dataplane)
105@todo Consider moving oft up a level
Dan Talayco2c0dba32010-03-06 22:47:06 -0800106
Dan Talayco1a88c122010-03-07 22:00:20 -0800107Current test case setup:
Dan Talayco2c0dba32010-03-06 22:47:06 -0800108 Files in this or sub directories (or later, directory specified on
109command line) that contain a function test_set_init are considered test
110files.
111 The function test_set_init examines the test_spec config variable
112and generates a suite of tests.
113 Support a command line option --test_mod so that all tests in that
114module will be run.
115 Support all to specify all tests from the module.
116
Dan Talayco48370102010-03-03 15:17:33 -0800117"""
118
119import sys
120from optparse import OptionParser
Dan Talayco2c0dba32010-03-06 22:47:06 -0800121from subprocess import Popen,PIPE
Dan Talayco48370102010-03-03 15:17:33 -0800122import logging
123import unittest
Dan Talayco2c0dba32010-03-06 22:47:06 -0800124import time
Brandon Heller446c1432010-04-01 12:43:27 -0700125import os
Dan Talayco48370102010-03-03 15:17:33 -0800126
Dan Talayco02eca0b2010-04-15 16:09:43 -0700127try:
128 import scapy.all as scapy
129except:
130 try:
131 import scapy as scapy
132 except:
133 sys.exit("Need to install scapy for packet parsing")
134
Dan Talayco48370102010-03-03 15:17:33 -0800135##@var DEBUG_LEVELS
136# Map from strings to debugging levels
137DEBUG_LEVELS = {
138 'debug' : logging.DEBUG,
139 'verbose' : logging.DEBUG,
140 'info' : logging.INFO,
141 'warning' : logging.WARNING,
142 'warn' : logging.WARNING,
143 'error' : logging.ERROR,
144 'critical' : logging.CRITICAL
145}
146
147_debug_default = "warning"
148_debug_level_default = DEBUG_LEVELS[_debug_default]
149
150##@var config_default
151# The default configuration dictionary for OFT
152config_default = {
Dan Talayco551befa2010-07-15 17:05:32 -0700153 "param" : None,
Dan Talayco48370102010-03-03 15:17:33 -0800154 "platform" : "local",
155 "controller_host" : "127.0.0.1",
156 "controller_port" : 6633,
157 "port_count" : 4,
158 "base_of_port" : 1,
159 "base_if_index" : 1,
Dan Talayco2c0dba32010-03-06 22:47:06 -0800160 "test_spec" : "all",
161 "test_dir" : ".",
Dan Talayco48370102010-03-03 15:17:33 -0800162 "log_file" : "oft.log",
Dan Talayco2c0dba32010-03-06 22:47:06 -0800163 "list" : False,
Dan Talayco48370102010-03-03 15:17:33 -0800164 "debug" : _debug_default,
165 "dbg_level" : _debug_level_default,
Dan Talaycoac25cf32010-07-20 14:08:28 -0700166 "port_map" : {},
167 "test_params" : "None"
Dan Talayco48370102010-03-03 15:17:33 -0800168}
169
Dan Talaycoc24aaae2010-07-08 14:05:24 -0700170# Default test priority
171TEST_PRIO_DEFAULT=100
172
Dan Talayco1a88c122010-03-07 22:00:20 -0800173#@todo Set up a dict of config params so easier to manage:
174# <param> <cmdline flags> <default value> <help> <optional parser>
175
Dan Talayco48370102010-03-03 15:17:33 -0800176# Map options to config structure
177def config_get(opts):
178 "Convert options class to OFT configuration dictionary"
179 cfg = config_default.copy()
Dan Talayco2c0dba32010-03-06 22:47:06 -0800180 for key in cfg.keys():
181 cfg[key] = eval("opts." + key)
182
183 # Special case checks
Dan Talayco48370102010-03-03 15:17:33 -0800184 if opts.debug not in DEBUG_LEVELS.keys():
185 print "Warning: Bad value specified for debug level; using default"
186 opts.debug = _debug_default
Dan Talayco02eca0b2010-04-15 16:09:43 -0700187 if opts.verbose:
188 cfg["debug"] = "verbose"
Dan Talayco48370102010-03-03 15:17:33 -0800189 cfg["dbg_level"] = DEBUG_LEVELS[cfg["debug"]]
Dan Talayco2c0dba32010-03-06 22:47:06 -0800190
Dan Talayco48370102010-03-03 15:17:33 -0800191 return cfg
192
193def config_setup(cfg_dflt):
194 """
195 Set up the configuration including parsing the arguments
196
197 @param cfg_dflt The default configuration dictionary
198 @return A pair (config, args) where config is an config
199 object and args is any additional arguments from the command line
200 """
201
202 parser = OptionParser(version="%prog 0.1")
203
Dan Talayco2c0dba32010-03-06 22:47:06 -0800204 #@todo parse port map as option?
Dan Talayco48370102010-03-03 15:17:33 -0800205 # Set up default values
Dan Talayco2c0dba32010-03-06 22:47:06 -0800206 for key in cfg_dflt.keys():
207 eval("parser.set_defaults("+key+"=cfg_dflt['"+key+"'])")
Dan Talayco48370102010-03-03 15:17:33 -0800208
Dan Talayco2c0dba32010-03-06 22:47:06 -0800209 #@todo Add options via dictionary
Dan Talayco48370102010-03-03 15:17:33 -0800210 plat_help = """Set the platform type. Valid values include:
211 local: User space virtual ethernet pair setup
212 remote: Remote embedded Broadcom based switch
Dan Talayco673e0852010-03-06 23:09:23 -0800213 Create a new_plat.py file and use --platform=new_plat on the command line
Dan Talayco48370102010-03-03 15:17:33 -0800214 """
215 parser.add_option("-P", "--platform", help=plat_help)
216 parser.add_option("-H", "--host", dest="controller_host",
217 help="The IP/name of the test controller host")
218 parser.add_option("-p", "--port", dest="controller_port",
219 type="int", help="Port number of the test controller")
Dan Talayco673e0852010-03-06 23:09:23 -0800220 test_list_help = """Indicate tests to run. Valid entries are "all" (the
221 default) or a comma separated list of:
222 module Run all tests in the named module
223 testcase Run tests in all modules with the name testcase
224 module.testcase Run the specific test case
225 """
226 parser.add_option("--test-spec", "--test-list", help=test_list_help)
Dan Talayco48370102010-03-03 15:17:33 -0800227 parser.add_option("--log-file",
228 help="Name of log file, empty string to log to console")
229 parser.add_option("--debug",
230 help="Debug lvl: debug, info, warning, error, critical")
Dan Talayco02eca0b2010-04-15 16:09:43 -0700231 parser.add_option("--port-count", type="int",
Dan Talayco48370102010-03-03 15:17:33 -0800232 help="Number of ports to use (optional)")
Dan Talayco02eca0b2010-04-15 16:09:43 -0700233 parser.add_option("--base-of-port", type="int",
Dan Talayco48370102010-03-03 15:17:33 -0800234 help="Base OpenFlow port number (optional)")
Dan Talayco02eca0b2010-04-15 16:09:43 -0700235 parser.add_option("--base-if-index", type="int",
Dan Talayco2c0dba32010-03-06 22:47:06 -0800236 help="Base interface index number (optional)")
237 parser.add_option("--list", action="store_true",
Brandon Heller824504e2010-04-01 12:21:37 -0700238 help="List all tests and exit")
Dan Talayco02eca0b2010-04-15 16:09:43 -0700239 parser.add_option("--verbose", action="store_true",
240 help="Short cut for --debug=verbose")
Dan Talayco551befa2010-07-15 17:05:32 -0700241 parser.add_option("--param", type="int",
242 help="Parameter sent to test (for debugging)")
Dan Talaycoac25cf32010-07-20 14:08:28 -0700243 parser.add_option("-t", "--test-params",
244 help="Set test parameters: key=val;... See --list")
Dan Talayco48370102010-03-03 15:17:33 -0800245 # Might need this if other parsers want command line
246 # parser.allow_interspersed_args = False
247 (options, args) = parser.parse_args()
248
249 config = config_get(options)
250
251 return (config, args)
252
253def logging_setup(config):
254 """
255 Set up logging based on config
256 """
257 _format = "%(asctime)s %(name)-10s: %(levelname)-8s: %(message)s"
258 _datefmt = "%H:%M:%S"
Dan Talayco88fc8802010-03-07 11:37:52 -0800259 logging.basicConfig(filename=config["log_file"],
260 level=config["dbg_level"],
261 format=_format, datefmt=_datefmt)
Dan Talayco48370102010-03-03 15:17:33 -0800262
263def default_port_map_setup(config):
264 """
265 Setup the OF port mapping based on config
266 @param config The OFT configuration structure
267 @return Port map dictionary
268 """
269 if (config["base_of_port"] is None) or not config["port_count"]:
270 return None
271 port_map = {}
272 if config["platform"] == "local":
273 # For local, use every other veth port
274 for idx in range(config["port_count"]):
275 port_map[config["base_of_port"] + idx] = "veth" + \
276 str(config["base_if_index"] + (2 * idx))
277 elif config["platform"] == "remote":
278 # For remote, use eth ports
279 for idx in range(config["port_count"]):
280 port_map[config["base_of_port"] + idx] = "eth" + \
281 str(config["base_if_index"] + idx)
282 else:
283 return None
284
285 logging.info("Built default port map")
286 return port_map
287
Dan Talayco2c0dba32010-03-06 22:47:06 -0800288def test_list_generate(config):
289 """Generate the list of all known tests indexed by module name
290
291 Conventions: Test files must implement the function test_set_init
292
Dan Talayco1a88c122010-03-07 22:00:20 -0800293 Test cases are classes that implement runTest
Dan Talayco2c0dba32010-03-06 22:47:06 -0800294
295 @param config The oft configuration dictionary
296 @returns An array of triples (mod-name, module, [tests]) where
297 mod-name is the string (filename) of the module, module is the
298 value returned from __import__'ing the module and [tests] is an
299 array of strings giving the test cases from the module.
300 """
301
302 # Find and import test files
303 p1 = Popen(["find", config["test_dir"], "-type","f"], stdout = PIPE)
304 p2 = Popen(["xargs", "grep", "-l", "-e", "^def test_set_init"],
305 stdin=p1.stdout, stdout=PIPE)
306
307 all_tests = {}
308 mod_name_map = {}
309 # There's an extra empty entry at the end of the list
310 filelist = p2.communicate()[0].split("\n")[:-1]
311 for file in filelist:
Dan Talaycoac25cf32010-07-20 14:08:28 -0700312 if file[-1:] == '~' or file[0] == '#':
Dan Talaycode2a6392010-03-10 13:56:51 -0800313 continue
Dan Talayco2c0dba32010-03-06 22:47:06 -0800314 modfile = file.lstrip('./')[:-3]
315
316 try:
317 mod = __import__(modfile)
318 except:
319 logging.warning("Could not import file " + file)
320 continue
321 mod_name_map[modfile] = mod
322 added_fn = False
323 for fn in dir(mod):
324 if 'runTest' in dir(eval("mod." + fn)):
325 if not added_fn:
326 mod_name_map[modfile] = mod
327 all_tests[mod] = []
328 added_fn = True
329 all_tests[mod].append(fn)
330 config["all_tests"] = all_tests
331 config["mod_name_map"] = mod_name_map
332
333def die(msg, exit_val=1):
334 print msg
335 logging.critical(msg)
336 sys.exit(exit_val)
337
338def add_test(suite, mod, name):
339 logging.info("Adding test " + mod.__name__ + "." + name)
340 suite.addTest(eval("mod." + name)())
341
Dan Talayco79f36082010-03-11 16:53:53 -0800342def _space_to(n, str):
343 """
344 Generate a string of spaces to achieve width n given string str
345 If length of str >= n, return one space
346 """
347 spaces = n - len(str)
348 if spaces > 0:
349 return " " * spaces
350 return " "
351
Dan Talaycoc24aaae2010-07-08 14:05:24 -0700352def test_prio_get(mod, test):
353 """
354 Return the priority of a test
355 If set in the test_prio variable for the module, return
356 that value. Otherwise return 100 (default)
357 """
358 if 'test_prio' in dir(mod):
359 if test in mod.test_prio.keys():
360 return mod.test_prio[test]
361 return TEST_PRIO_DEFAULT
362
Dan Talayco48370102010-03-03 15:17:33 -0800363#
364# Main script
365#
366
367# Get configuration, set up logging, import platform from file
368(config, args) = config_setup(config_default)
Dan Talayco48370102010-03-03 15:17:33 -0800369
Dan Talayco2c0dba32010-03-06 22:47:06 -0800370test_list_generate(config)
371
372# Check if test list is requested; display and exit if so
373if config["list"]:
Dan Talayco79f36082010-03-11 16:53:53 -0800374 did_print = False
Dan Talayco2c0dba32010-03-06 22:47:06 -0800375 print "\nTest List:"
376 for mod in config["all_tests"].keys():
Dan Talayco79f36082010-03-11 16:53:53 -0800377 if config["test_spec"] != "all" and \
378 config["test_spec"] != mod.__name__:
379 continue
380 did_print = True
381 desc = mod.__doc__.strip()
382 desc = desc.split('\n')[0]
383 start_str = " Module " + mod.__name__ + ": "
384 print start_str + _space_to(22, start_str) + desc
Dan Talayco2c0dba32010-03-06 22:47:06 -0800385 for test in config["all_tests"][mod]:
Dan Talayco551befa2010-07-15 17:05:32 -0700386 try:
387 desc = eval('mod.' + test + '.__doc__.strip()')
388 desc = desc.split('\n')[0]
389 except:
390 desc = "No description"
Dan Talaycoc24aaae2010-07-08 14:05:24 -0700391 if test_prio_get(mod, test) < 0:
392 start_str = " * " + test + ":"
393 else:
394 start_str = " " + test + ":"
Dan Talayco551befa2010-07-15 17:05:32 -0700395 if len(start_str) > 22:
396 desc = "\n" + _space_to(22, "") + desc
Dan Talayco79f36082010-03-11 16:53:53 -0800397 print start_str + _space_to(22, start_str) + desc
398 print
399 if not did_print:
400 print "No tests found for " + config["test_spec"]
Dan Talaycoc24aaae2010-07-08 14:05:24 -0700401 else:
Dan Talayco7aa0b812010-07-20 14:51:41 -0700402 print "Tests preceded by * are not run by default"
403 print "Tests marked (TP1) after name take --test-params including:"
Dan Talaycoac25cf32010-07-20 14:08:28 -0700404 print " 'vid=N;strip_vlan=bool;add_vlan=bool'"
Dan Talayco2c0dba32010-03-06 22:47:06 -0800405 sys.exit(0)
406
407logging_setup(config)
408logging.info("++++++++ " + time.asctime() + " ++++++++")
409
410# Generate the test suite
411#@todo Decide if multiple suites are ever needed
412suite = unittest.TestSuite()
413
Dan Talaycoc24aaae2010-07-08 14:05:24 -0700414#@todo Allow specification of priority to override prio check
Dan Talayco2c0dba32010-03-06 22:47:06 -0800415if config["test_spec"] == "all":
416 for mod in config["all_tests"].keys():
417 for test in config["all_tests"][mod]:
Dan Talaycoc24aaae2010-07-08 14:05:24 -0700418 # For now, a way to avoid tests
419 if test_prio_get(mod, test) >= 0:
420 add_test(suite, mod, test)
Dan Talayco2c0dba32010-03-06 22:47:06 -0800421
422else:
423 for ts_entry in config["test_spec"].split(","):
424 parts = ts_entry.split(".")
425
426 if len(parts) == 1: # Either a module or test name
427 if ts_entry in config["mod_name_map"].keys():
428 mod = config["mod_name_map"][ts_entry]
429 for test in config["all_tests"][mod]:
430 add_test(suite, mod, test)
431 else: # Search for matching tests
432 test_found = False
433 for mod in config["all_tests"].keys():
434 if ts_entry in config["all_tests"][mod]:
435 add_test(suite, mod, ts_entry)
436 test_found = True
437 if not test_found:
438 die("Could not find module or test: " + ts_entry)
439
440 elif len(parts) == 2: # module.test
441 if parts[0] not in config["mod_name_map"]:
442 die("Unknown module in test spec: " + ts_entry)
443 mod = config["mod_name_map"][parts[0]]
444 if parts[1] in config["all_tests"][mod]:
445 add_test(suite, mod, parts[1])
446 else:
447 die("No known test matches: " + ts_entry)
448
449 else:
450 die("Bad test spec: " + ts_entry)
451
452# Check if platform specified
Dan Talayco48370102010-03-03 15:17:33 -0800453if config["platform"]:
454 _imp_string = "from " + config["platform"] + " import *"
Dan Talayco2c0dba32010-03-06 22:47:06 -0800455 logging.info("Importing platform: " + _imp_string)
Dan Talayco48370102010-03-03 15:17:33 -0800456 try:
457 exec(_imp_string)
458 except:
459 logging.warn("Failed to import " + config["platform"] + " file")
460
461try:
462 platform_config_update(config)
463except:
464 logging.warn("Could not run platform host configuration")
465
466if not config["port_map"]:
467 # Try to set up default port mapping if not done by platform
468 config["port_map"] = default_port_map_setup(config)
469
470if not config["port_map"]:
Dan Talayco2c0dba32010-03-06 22:47:06 -0800471 die("Interface port map is not defined. Exiting")
Dan Talayco48370102010-03-03 15:17:33 -0800472
473logging.debug("Configuration: " + str(config))
474logging.info("OF port map: " + str(config["port_map"]))
475
476# Init the test sets
Dan Talayco2c0dba32010-03-06 22:47:06 -0800477for (modname,mod) in config["mod_name_map"].items():
478 try:
479 mod.test_set_init(config)
480 except:
481 logging.warning("Could not run test_set_init for " + modname)
Dan Talayco48370102010-03-03 15:17:33 -0800482
Dan Talayco2c0dba32010-03-06 22:47:06 -0800483if config["dbg_level"] == logging.CRITICAL:
484 _verb = 0
485elif config["dbg_level"] >= logging.WARNING:
486 _verb = 1
487else:
488 _verb = 2
Dan Talayco48370102010-03-03 15:17:33 -0800489
Brandon Heller446c1432010-04-01 12:43:27 -0700490if os.getuid() != 0:
491 print "ERROR: Super-user privileges required. Please re-run with " \
492 "sudo or as root."
493 exit(1)
494
Dan Talaycoac25cf32010-07-20 14:08:28 -0700495
496if __name__ == "__main__":
497 logging.info("*** TEST RUN START: " + time.asctime())
498 unittest.TextTestRunner(verbosity=_verb).run(suite)
499 logging.info("*** TEST RUN END : " + time.asctime())
Dan Talayco48370102010-03-03 15:17:33 -0800500