blob: b4314fe0dc3cd3750521c1465424a231a08e911f [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 = {
153 "platform" : "local",
154 "controller_host" : "127.0.0.1",
155 "controller_port" : 6633,
156 "port_count" : 4,
157 "base_of_port" : 1,
158 "base_if_index" : 1,
Dan Talayco2c0dba32010-03-06 22:47:06 -0800159 "test_spec" : "all",
160 "test_dir" : ".",
Dan Talayco48370102010-03-03 15:17:33 -0800161 "log_file" : "oft.log",
Dan Talayco2c0dba32010-03-06 22:47:06 -0800162 "list" : False,
Dan Talayco48370102010-03-03 15:17:33 -0800163 "debug" : _debug_default,
164 "dbg_level" : _debug_level_default,
165 "port_map" : {}
166}
167
Dan Talaycoc24aaae2010-07-08 14:05:24 -0700168# Default test priority
169TEST_PRIO_DEFAULT=100
170
Dan Talayco1a88c122010-03-07 22:00:20 -0800171#@todo Set up a dict of config params so easier to manage:
172# <param> <cmdline flags> <default value> <help> <optional parser>
173
Dan Talayco48370102010-03-03 15:17:33 -0800174# Map options to config structure
175def config_get(opts):
176 "Convert options class to OFT configuration dictionary"
177 cfg = config_default.copy()
Dan Talayco2c0dba32010-03-06 22:47:06 -0800178 for key in cfg.keys():
179 cfg[key] = eval("opts." + key)
180
181 # Special case checks
Dan Talayco48370102010-03-03 15:17:33 -0800182 if opts.debug not in DEBUG_LEVELS.keys():
183 print "Warning: Bad value specified for debug level; using default"
184 opts.debug = _debug_default
Dan Talayco02eca0b2010-04-15 16:09:43 -0700185 if opts.verbose:
186 cfg["debug"] = "verbose"
Dan Talayco48370102010-03-03 15:17:33 -0800187 cfg["dbg_level"] = DEBUG_LEVELS[cfg["debug"]]
Dan Talayco2c0dba32010-03-06 22:47:06 -0800188
Dan Talayco48370102010-03-03 15:17:33 -0800189 return cfg
190
191def config_setup(cfg_dflt):
192 """
193 Set up the configuration including parsing the arguments
194
195 @param cfg_dflt The default configuration dictionary
196 @return A pair (config, args) where config is an config
197 object and args is any additional arguments from the command line
198 """
199
200 parser = OptionParser(version="%prog 0.1")
201
Dan Talayco2c0dba32010-03-06 22:47:06 -0800202 #@todo parse port map as option?
Dan Talayco48370102010-03-03 15:17:33 -0800203 # Set up default values
Dan Talayco2c0dba32010-03-06 22:47:06 -0800204 for key in cfg_dflt.keys():
205 eval("parser.set_defaults("+key+"=cfg_dflt['"+key+"'])")
Dan Talayco48370102010-03-03 15:17:33 -0800206
Dan Talayco2c0dba32010-03-06 22:47:06 -0800207 #@todo Add options via dictionary
Dan Talayco48370102010-03-03 15:17:33 -0800208 plat_help = """Set the platform type. Valid values include:
209 local: User space virtual ethernet pair setup
210 remote: Remote embedded Broadcom based switch
Dan Talayco673e0852010-03-06 23:09:23 -0800211 Create a new_plat.py file and use --platform=new_plat on the command line
Dan Talayco48370102010-03-03 15:17:33 -0800212 """
213 parser.add_option("-P", "--platform", help=plat_help)
214 parser.add_option("-H", "--host", dest="controller_host",
215 help="The IP/name of the test controller host")
216 parser.add_option("-p", "--port", dest="controller_port",
217 type="int", help="Port number of the test controller")
Dan Talayco673e0852010-03-06 23:09:23 -0800218 test_list_help = """Indicate tests to run. Valid entries are "all" (the
219 default) or a comma separated list of:
220 module Run all tests in the named module
221 testcase Run tests in all modules with the name testcase
222 module.testcase Run the specific test case
223 """
224 parser.add_option("--test-spec", "--test-list", help=test_list_help)
Dan Talayco48370102010-03-03 15:17:33 -0800225 parser.add_option("--log-file",
226 help="Name of log file, empty string to log to console")
227 parser.add_option("--debug",
228 help="Debug lvl: debug, info, warning, error, critical")
Dan Talayco02eca0b2010-04-15 16:09:43 -0700229 parser.add_option("--port-count", type="int",
Dan Talayco48370102010-03-03 15:17:33 -0800230 help="Number of ports to use (optional)")
Dan Talayco02eca0b2010-04-15 16:09:43 -0700231 parser.add_option("--base-of-port", type="int",
Dan Talayco48370102010-03-03 15:17:33 -0800232 help="Base OpenFlow port number (optional)")
Dan Talayco02eca0b2010-04-15 16:09:43 -0700233 parser.add_option("--base-if-index", type="int",
Dan Talayco2c0dba32010-03-06 22:47:06 -0800234 help="Base interface index number (optional)")
235 parser.add_option("--list", action="store_true",
Brandon Heller824504e2010-04-01 12:21:37 -0700236 help="List all tests and exit")
Dan Talayco02eca0b2010-04-15 16:09:43 -0700237 parser.add_option("--verbose", action="store_true",
238 help="Short cut for --debug=verbose")
Dan Talayco48370102010-03-03 15:17:33 -0800239 # Might need this if other parsers want command line
240 # parser.allow_interspersed_args = False
241 (options, args) = parser.parse_args()
242
243 config = config_get(options)
244
245 return (config, args)
246
247def logging_setup(config):
248 """
249 Set up logging based on config
250 """
251 _format = "%(asctime)s %(name)-10s: %(levelname)-8s: %(message)s"
252 _datefmt = "%H:%M:%S"
Dan Talayco88fc8802010-03-07 11:37:52 -0800253 logging.basicConfig(filename=config["log_file"],
254 level=config["dbg_level"],
255 format=_format, datefmt=_datefmt)
Dan Talayco48370102010-03-03 15:17:33 -0800256
257def default_port_map_setup(config):
258 """
259 Setup the OF port mapping based on config
260 @param config The OFT configuration structure
261 @return Port map dictionary
262 """
263 if (config["base_of_port"] is None) or not config["port_count"]:
264 return None
265 port_map = {}
266 if config["platform"] == "local":
267 # For local, use every other veth port
268 for idx in range(config["port_count"]):
269 port_map[config["base_of_port"] + idx] = "veth" + \
270 str(config["base_if_index"] + (2 * idx))
271 elif config["platform"] == "remote":
272 # For remote, use eth ports
273 for idx in range(config["port_count"]):
274 port_map[config["base_of_port"] + idx] = "eth" + \
275 str(config["base_if_index"] + idx)
276 else:
277 return None
278
279 logging.info("Built default port map")
280 return port_map
281
Dan Talayco2c0dba32010-03-06 22:47:06 -0800282def test_list_generate(config):
283 """Generate the list of all known tests indexed by module name
284
285 Conventions: Test files must implement the function test_set_init
286
Dan Talayco1a88c122010-03-07 22:00:20 -0800287 Test cases are classes that implement runTest
Dan Talayco2c0dba32010-03-06 22:47:06 -0800288
289 @param config The oft configuration dictionary
290 @returns An array of triples (mod-name, module, [tests]) where
291 mod-name is the string (filename) of the module, module is the
292 value returned from __import__'ing the module and [tests] is an
293 array of strings giving the test cases from the module.
294 """
295
296 # Find and import test files
297 p1 = Popen(["find", config["test_dir"], "-type","f"], stdout = PIPE)
298 p2 = Popen(["xargs", "grep", "-l", "-e", "^def test_set_init"],
299 stdin=p1.stdout, stdout=PIPE)
300
301 all_tests = {}
302 mod_name_map = {}
303 # There's an extra empty entry at the end of the list
304 filelist = p2.communicate()[0].split("\n")[:-1]
305 for file in filelist:
Dan Talaycode2a6392010-03-10 13:56:51 -0800306 if file[-1:] == '~':
307 continue
Dan Talayco2c0dba32010-03-06 22:47:06 -0800308 modfile = file.lstrip('./')[:-3]
309
310 try:
311 mod = __import__(modfile)
312 except:
313 logging.warning("Could not import file " + file)
314 continue
315 mod_name_map[modfile] = mod
316 added_fn = False
317 for fn in dir(mod):
318 if 'runTest' in dir(eval("mod." + fn)):
319 if not added_fn:
320 mod_name_map[modfile] = mod
321 all_tests[mod] = []
322 added_fn = True
323 all_tests[mod].append(fn)
324 config["all_tests"] = all_tests
325 config["mod_name_map"] = mod_name_map
326
327def die(msg, exit_val=1):
328 print msg
329 logging.critical(msg)
330 sys.exit(exit_val)
331
332def add_test(suite, mod, name):
333 logging.info("Adding test " + mod.__name__ + "." + name)
334 suite.addTest(eval("mod." + name)())
335
Dan Talayco79f36082010-03-11 16:53:53 -0800336def _space_to(n, str):
337 """
338 Generate a string of spaces to achieve width n given string str
339 If length of str >= n, return one space
340 """
341 spaces = n - len(str)
342 if spaces > 0:
343 return " " * spaces
344 return " "
345
Dan Talaycoc24aaae2010-07-08 14:05:24 -0700346def test_prio_get(mod, test):
347 """
348 Return the priority of a test
349 If set in the test_prio variable for the module, return
350 that value. Otherwise return 100 (default)
351 """
352 if 'test_prio' in dir(mod):
353 if test in mod.test_prio.keys():
354 return mod.test_prio[test]
355 return TEST_PRIO_DEFAULT
356
Dan Talayco48370102010-03-03 15:17:33 -0800357#
358# Main script
359#
360
361# Get configuration, set up logging, import platform from file
362(config, args) = config_setup(config_default)
Dan Talayco48370102010-03-03 15:17:33 -0800363
Dan Talayco2c0dba32010-03-06 22:47:06 -0800364test_list_generate(config)
365
366# Check if test list is requested; display and exit if so
367if config["list"]:
Dan Talayco79f36082010-03-11 16:53:53 -0800368 did_print = False
Dan Talayco2c0dba32010-03-06 22:47:06 -0800369 print "\nTest List:"
370 for mod in config["all_tests"].keys():
Dan Talayco79f36082010-03-11 16:53:53 -0800371 if config["test_spec"] != "all" and \
372 config["test_spec"] != mod.__name__:
373 continue
374 did_print = True
375 desc = mod.__doc__.strip()
376 desc = desc.split('\n')[0]
377 start_str = " Module " + mod.__name__ + ": "
378 print start_str + _space_to(22, start_str) + desc
Dan Talayco2c0dba32010-03-06 22:47:06 -0800379 for test in config["all_tests"][mod]:
Dan Talayco79f36082010-03-11 16:53:53 -0800380 desc = eval('mod.' + test + '.__doc__.strip()')
381 desc = desc.split('\n')[0]
Dan Talaycoc24aaae2010-07-08 14:05:24 -0700382 if test_prio_get(mod, test) < 0:
383 start_str = " * " + test + ":"
384 else:
385 start_str = " " + test + ":"
Dan Talayco79f36082010-03-11 16:53:53 -0800386 print start_str + _space_to(22, start_str) + desc
387 print
388 if not did_print:
389 print "No tests found for " + config["test_spec"]
Dan Talaycoc24aaae2010-07-08 14:05:24 -0700390 else:
391 print "Tests marked * are not run by default"
Dan Talayco2c0dba32010-03-06 22:47:06 -0800392 sys.exit(0)
393
394logging_setup(config)
395logging.info("++++++++ " + time.asctime() + " ++++++++")
396
397# Generate the test suite
398#@todo Decide if multiple suites are ever needed
399suite = unittest.TestSuite()
400
Dan Talaycoc24aaae2010-07-08 14:05:24 -0700401#@todo Allow specification of priority to override prio check
Dan Talayco2c0dba32010-03-06 22:47:06 -0800402if config["test_spec"] == "all":
403 for mod in config["all_tests"].keys():
404 for test in config["all_tests"][mod]:
Dan Talaycoc24aaae2010-07-08 14:05:24 -0700405 # For now, a way to avoid tests
406 if test_prio_get(mod, test) >= 0:
407 add_test(suite, mod, test)
Dan Talayco2c0dba32010-03-06 22:47:06 -0800408
409else:
410 for ts_entry in config["test_spec"].split(","):
411 parts = ts_entry.split(".")
412
413 if len(parts) == 1: # Either a module or test name
414 if ts_entry in config["mod_name_map"].keys():
415 mod = config["mod_name_map"][ts_entry]
416 for test in config["all_tests"][mod]:
417 add_test(suite, mod, test)
418 else: # Search for matching tests
419 test_found = False
420 for mod in config["all_tests"].keys():
421 if ts_entry in config["all_tests"][mod]:
422 add_test(suite, mod, ts_entry)
423 test_found = True
424 if not test_found:
425 die("Could not find module or test: " + ts_entry)
426
427 elif len(parts) == 2: # module.test
428 if parts[0] not in config["mod_name_map"]:
429 die("Unknown module in test spec: " + ts_entry)
430 mod = config["mod_name_map"][parts[0]]
431 if parts[1] in config["all_tests"][mod]:
432 add_test(suite, mod, parts[1])
433 else:
434 die("No known test matches: " + ts_entry)
435
436 else:
437 die("Bad test spec: " + ts_entry)
438
439# Check if platform specified
Dan Talayco48370102010-03-03 15:17:33 -0800440if config["platform"]:
441 _imp_string = "from " + config["platform"] + " import *"
Dan Talayco2c0dba32010-03-06 22:47:06 -0800442 logging.info("Importing platform: " + _imp_string)
Dan Talayco48370102010-03-03 15:17:33 -0800443 try:
444 exec(_imp_string)
445 except:
446 logging.warn("Failed to import " + config["platform"] + " file")
447
448try:
449 platform_config_update(config)
450except:
451 logging.warn("Could not run platform host configuration")
452
453if not config["port_map"]:
454 # Try to set up default port mapping if not done by platform
455 config["port_map"] = default_port_map_setup(config)
456
457if not config["port_map"]:
Dan Talayco2c0dba32010-03-06 22:47:06 -0800458 die("Interface port map is not defined. Exiting")
Dan Talayco48370102010-03-03 15:17:33 -0800459
460logging.debug("Configuration: " + str(config))
461logging.info("OF port map: " + str(config["port_map"]))
462
463# Init the test sets
Dan Talayco2c0dba32010-03-06 22:47:06 -0800464for (modname,mod) in config["mod_name_map"].items():
465 try:
466 mod.test_set_init(config)
467 except:
468 logging.warning("Could not run test_set_init for " + modname)
Dan Talayco48370102010-03-03 15:17:33 -0800469
Dan Talayco2c0dba32010-03-06 22:47:06 -0800470if config["dbg_level"] == logging.CRITICAL:
471 _verb = 0
472elif config["dbg_level"] >= logging.WARNING:
473 _verb = 1
474else:
475 _verb = 2
Dan Talayco48370102010-03-03 15:17:33 -0800476
Brandon Heller446c1432010-04-01 12:43:27 -0700477if os.getuid() != 0:
478 print "ERROR: Super-user privileges required. Please re-run with " \
479 "sudo or as root."
480 exit(1)
481
Dan Talayco2c0dba32010-03-06 22:47:06 -0800482logging.info("*** TEST RUN START: " + time.asctime())
483unittest.TextTestRunner(verbosity=_verb).run(suite)
484logging.info("*** TEST RUN END : " + time.asctime())
Dan Talayco48370102010-03-03 15:17:33 -0800485