blob: c851ae9f832e0ec5412551c3379a7337faf4a856 [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
42To add a test to the system, either: edit an existing test case file (like
43basic.py) to add a test class which inherits from unittest.TestCase (directly
44or indirectly); or add a new file which includes a function definition
45test_set_init(config). Preferably the file is in the same directory as existing
46tests, though you can specify the directory on the command line. The file
47should not be called "all" as that's reserved for the test-spec.
48
49If you add a new file, the test_set_init function should record the port
50map object from the configuration along with whatever other configuration
51information it may need.
52
53TBD: To add configuration to the system, first add an entry to config_default
Dan Talayco48370102010-03-03 15:17:33 -080054below. If you want this to be a command line parameter, edit config_setup
55to add the option and default value to the parser. Then edit config_get
56to make sure the option value gets copied into the configuration
57structure (which then gets passed to everyone else).
58
59By convention, oft attempts to import the contents of a file by the
60name of $platform.py into the local namespace.
61
62IMPORTANT: That file should define a function platform_config_update which
63takes a configuration dictionary as an argument and updates it for the
64current run. In particular, it should set up config["port_map"] with
65the proper map from OF port numbers to OF interface names.
66
67You can add your own platform, say gp104, by adding a file gp104.py
68that defines the function platform_config_update and then use the
69parameter --platform=gp104 on the command line.
70
71If platform_config_update does not set config["port_map"], an attempt
72is made to generate a default map via the function default_port_map_setup.
73This will use "local" and "remote" for platform names if available
74and generate a sequential map based on the values of base_of_port and
75base_if_index in the configuration structure.
76
Dan Talayco48370102010-03-03 15:17:33 -080077The current model for test sets is basic.py. The current convention is
78that the test set should implement a function test_set_init which takes
79an oft configuration dictionary and returns a unittest.TestSuite object.
80Future test sets should do the same thing.
81
Dan Talayco52f64442010-03-03 15:32:41 -080082Default setup:
83
84The default setup runs locally using veth pairs. To exercise this,
85checkout and build an openflow userspace datapath. Then start it on
86the local host:
Dan Talayco2c0dba32010-03-06 22:47:06 -080087<pre>
Dan Talayco52f64442010-03-03 15:32:41 -080088 sudo ~/openflow/regress/bin/veth_setup.pl
89 sudo ofdatapath -i veth0,veth2,veth4,veth6 punix:/tmp/ofd &
90 sudo ofprotocol unix:/tmp/ofd tcp:127.0.0.1 --fail=closed --max-backoff=1 &
91
92Next, run oft:
93 sudo ./oft --debug=info
Dan Talayco2c0dba32010-03-06 22:47:06 -080094</pre>
Dan Talayco52f64442010-03-03 15:32:41 -080095
96Examine oft.log if things don't work.
Dan Talayco2c0dba32010-03-06 22:47:06 -080097
Dan Talayco1a88c122010-03-07 22:00:20 -080098@todo Support per-component debug levels (esp controller vs dataplane)
99@todo Consider moving oft up a level
Dan Talayco2c0dba32010-03-06 22:47:06 -0800100
Dan Talayco1a88c122010-03-07 22:00:20 -0800101Current test case setup:
Dan Talayco2c0dba32010-03-06 22:47:06 -0800102 Files in this or sub directories (or later, directory specified on
103command line) that contain a function test_set_init are considered test
104files.
105 The function test_set_init examines the test_spec config variable
106and generates a suite of tests.
107 Support a command line option --test_mod so that all tests in that
108module will be run.
109 Support all to specify all tests from the module.
110
Dan Talayco48370102010-03-03 15:17:33 -0800111"""
112
113import sys
114from optparse import OptionParser
Dan Talayco2c0dba32010-03-06 22:47:06 -0800115from subprocess import Popen,PIPE
Dan Talayco48370102010-03-03 15:17:33 -0800116import logging
117import unittest
Dan Talayco2c0dba32010-03-06 22:47:06 -0800118import time
Brandon Heller446c1432010-04-01 12:43:27 -0700119import os
Dan Talayco48370102010-03-03 15:17:33 -0800120
Dan Talayco02eca0b2010-04-15 16:09:43 -0700121try:
122 import scapy.all as scapy
123except:
124 try:
125 import scapy as scapy
126 except:
127 sys.exit("Need to install scapy for packet parsing")
128
Dan Talayco48370102010-03-03 15:17:33 -0800129##@var DEBUG_LEVELS
130# Map from strings to debugging levels
131DEBUG_LEVELS = {
132 'debug' : logging.DEBUG,
133 'verbose' : logging.DEBUG,
134 'info' : logging.INFO,
135 'warning' : logging.WARNING,
136 'warn' : logging.WARNING,
137 'error' : logging.ERROR,
138 'critical' : logging.CRITICAL
139}
140
141_debug_default = "warning"
142_debug_level_default = DEBUG_LEVELS[_debug_default]
143
144##@var config_default
145# The default configuration dictionary for OFT
146config_default = {
147 "platform" : "local",
148 "controller_host" : "127.0.0.1",
149 "controller_port" : 6633,
150 "port_count" : 4,
151 "base_of_port" : 1,
152 "base_if_index" : 1,
Dan Talayco2c0dba32010-03-06 22:47:06 -0800153 "test_spec" : "all",
154 "test_dir" : ".",
Dan Talayco48370102010-03-03 15:17:33 -0800155 "log_file" : "oft.log",
Dan Talayco2c0dba32010-03-06 22:47:06 -0800156 "list" : False,
Dan Talayco48370102010-03-03 15:17:33 -0800157 "debug" : _debug_default,
158 "dbg_level" : _debug_level_default,
159 "port_map" : {}
160}
161
Dan Talayco1a88c122010-03-07 22:00:20 -0800162#@todo Set up a dict of config params so easier to manage:
163# <param> <cmdline flags> <default value> <help> <optional parser>
164
Dan Talayco48370102010-03-03 15:17:33 -0800165# Map options to config structure
166def config_get(opts):
167 "Convert options class to OFT configuration dictionary"
168 cfg = config_default.copy()
Dan Talayco2c0dba32010-03-06 22:47:06 -0800169 for key in cfg.keys():
170 cfg[key] = eval("opts." + key)
171
172 # Special case checks
Dan Talayco48370102010-03-03 15:17:33 -0800173 if opts.debug not in DEBUG_LEVELS.keys():
174 print "Warning: Bad value specified for debug level; using default"
175 opts.debug = _debug_default
Dan Talayco02eca0b2010-04-15 16:09:43 -0700176 if opts.verbose:
177 cfg["debug"] = "verbose"
Dan Talayco48370102010-03-03 15:17:33 -0800178 cfg["dbg_level"] = DEBUG_LEVELS[cfg["debug"]]
Dan Talayco2c0dba32010-03-06 22:47:06 -0800179
Dan Talayco48370102010-03-03 15:17:33 -0800180 return cfg
181
182def config_setup(cfg_dflt):
183 """
184 Set up the configuration including parsing the arguments
185
186 @param cfg_dflt The default configuration dictionary
187 @return A pair (config, args) where config is an config
188 object and args is any additional arguments from the command line
189 """
190
191 parser = OptionParser(version="%prog 0.1")
192
Dan Talayco2c0dba32010-03-06 22:47:06 -0800193 #@todo parse port map as option?
Dan Talayco48370102010-03-03 15:17:33 -0800194 # Set up default values
Dan Talayco2c0dba32010-03-06 22:47:06 -0800195 for key in cfg_dflt.keys():
196 eval("parser.set_defaults("+key+"=cfg_dflt['"+key+"'])")
Dan Talayco48370102010-03-03 15:17:33 -0800197
Dan Talayco2c0dba32010-03-06 22:47:06 -0800198 #@todo Add options via dictionary
Dan Talayco48370102010-03-03 15:17:33 -0800199 plat_help = """Set the platform type. Valid values include:
200 local: User space virtual ethernet pair setup
201 remote: Remote embedded Broadcom based switch
Dan Talayco673e0852010-03-06 23:09:23 -0800202 Create a new_plat.py file and use --platform=new_plat on the command line
Dan Talayco48370102010-03-03 15:17:33 -0800203 """
204 parser.add_option("-P", "--platform", help=plat_help)
205 parser.add_option("-H", "--host", dest="controller_host",
206 help="The IP/name of the test controller host")
207 parser.add_option("-p", "--port", dest="controller_port",
208 type="int", help="Port number of the test controller")
Dan Talayco673e0852010-03-06 23:09:23 -0800209 test_list_help = """Indicate tests to run. Valid entries are "all" (the
210 default) or a comma separated list of:
211 module Run all tests in the named module
212 testcase Run tests in all modules with the name testcase
213 module.testcase Run the specific test case
214 """
215 parser.add_option("--test-spec", "--test-list", help=test_list_help)
Dan Talayco48370102010-03-03 15:17:33 -0800216 parser.add_option("--log-file",
217 help="Name of log file, empty string to log to console")
218 parser.add_option("--debug",
219 help="Debug lvl: debug, info, warning, error, critical")
Dan Talayco02eca0b2010-04-15 16:09:43 -0700220 parser.add_option("--port-count", type="int",
Dan Talayco48370102010-03-03 15:17:33 -0800221 help="Number of ports to use (optional)")
Dan Talayco02eca0b2010-04-15 16:09:43 -0700222 parser.add_option("--base-of-port", type="int",
Dan Talayco48370102010-03-03 15:17:33 -0800223 help="Base OpenFlow port number (optional)")
Dan Talayco02eca0b2010-04-15 16:09:43 -0700224 parser.add_option("--base-if-index", type="int",
Dan Talayco2c0dba32010-03-06 22:47:06 -0800225 help="Base interface index number (optional)")
226 parser.add_option("--list", action="store_true",
Brandon Heller824504e2010-04-01 12:21:37 -0700227 help="List all tests and exit")
Dan Talayco02eca0b2010-04-15 16:09:43 -0700228 parser.add_option("--verbose", action="store_true",
229 help="Short cut for --debug=verbose")
Dan Talayco48370102010-03-03 15:17:33 -0800230 # Might need this if other parsers want command line
231 # parser.allow_interspersed_args = False
232 (options, args) = parser.parse_args()
233
234 config = config_get(options)
235
236 return (config, args)
237
238def logging_setup(config):
239 """
240 Set up logging based on config
241 """
242 _format = "%(asctime)s %(name)-10s: %(levelname)-8s: %(message)s"
243 _datefmt = "%H:%M:%S"
Dan Talayco88fc8802010-03-07 11:37:52 -0800244 logging.basicConfig(filename=config["log_file"],
245 level=config["dbg_level"],
246 format=_format, datefmt=_datefmt)
Dan Talayco48370102010-03-03 15:17:33 -0800247
248def default_port_map_setup(config):
249 """
250 Setup the OF port mapping based on config
251 @param config The OFT configuration structure
252 @return Port map dictionary
253 """
254 if (config["base_of_port"] is None) or not config["port_count"]:
255 return None
256 port_map = {}
257 if config["platform"] == "local":
258 # For local, use every other veth port
259 for idx in range(config["port_count"]):
260 port_map[config["base_of_port"] + idx] = "veth" + \
261 str(config["base_if_index"] + (2 * idx))
262 elif config["platform"] == "remote":
263 # For remote, use eth ports
264 for idx in range(config["port_count"]):
265 port_map[config["base_of_port"] + idx] = "eth" + \
266 str(config["base_if_index"] + idx)
267 else:
268 return None
269
270 logging.info("Built default port map")
271 return port_map
272
Dan Talayco2c0dba32010-03-06 22:47:06 -0800273def test_list_generate(config):
274 """Generate the list of all known tests indexed by module name
275
276 Conventions: Test files must implement the function test_set_init
277
Dan Talayco1a88c122010-03-07 22:00:20 -0800278 Test cases are classes that implement runTest
Dan Talayco2c0dba32010-03-06 22:47:06 -0800279
280 @param config The oft configuration dictionary
281 @returns An array of triples (mod-name, module, [tests]) where
282 mod-name is the string (filename) of the module, module is the
283 value returned from __import__'ing the module and [tests] is an
284 array of strings giving the test cases from the module.
285 """
286
287 # Find and import test files
288 p1 = Popen(["find", config["test_dir"], "-type","f"], stdout = PIPE)
289 p2 = Popen(["xargs", "grep", "-l", "-e", "^def test_set_init"],
290 stdin=p1.stdout, stdout=PIPE)
291
292 all_tests = {}
293 mod_name_map = {}
294 # There's an extra empty entry at the end of the list
295 filelist = p2.communicate()[0].split("\n")[:-1]
296 for file in filelist:
Dan Talaycode2a6392010-03-10 13:56:51 -0800297 if file[-1:] == '~':
298 continue
Dan Talayco2c0dba32010-03-06 22:47:06 -0800299 modfile = file.lstrip('./')[:-3]
300
301 try:
302 mod = __import__(modfile)
303 except:
304 logging.warning("Could not import file " + file)
305 continue
306 mod_name_map[modfile] = mod
307 added_fn = False
308 for fn in dir(mod):
309 if 'runTest' in dir(eval("mod." + fn)):
310 if not added_fn:
311 mod_name_map[modfile] = mod
312 all_tests[mod] = []
313 added_fn = True
314 all_tests[mod].append(fn)
315 config["all_tests"] = all_tests
316 config["mod_name_map"] = mod_name_map
317
318def die(msg, exit_val=1):
319 print msg
320 logging.critical(msg)
321 sys.exit(exit_val)
322
323def add_test(suite, mod, name):
324 logging.info("Adding test " + mod.__name__ + "." + name)
325 suite.addTest(eval("mod." + name)())
326
Dan Talayco79f36082010-03-11 16:53:53 -0800327def _space_to(n, str):
328 """
329 Generate a string of spaces to achieve width n given string str
330 If length of str >= n, return one space
331 """
332 spaces = n - len(str)
333 if spaces > 0:
334 return " " * spaces
335 return " "
336
Dan Talayco48370102010-03-03 15:17:33 -0800337#
338# Main script
339#
340
341# Get configuration, set up logging, import platform from file
342(config, args) = config_setup(config_default)
Dan Talayco48370102010-03-03 15:17:33 -0800343
Dan Talayco2c0dba32010-03-06 22:47:06 -0800344test_list_generate(config)
345
346# Check if test list is requested; display and exit if so
347if config["list"]:
Dan Talayco79f36082010-03-11 16:53:53 -0800348 did_print = False
Dan Talayco2c0dba32010-03-06 22:47:06 -0800349 print "\nTest List:"
350 for mod in config["all_tests"].keys():
Dan Talayco79f36082010-03-11 16:53:53 -0800351 if config["test_spec"] != "all" and \
352 config["test_spec"] != mod.__name__:
353 continue
354 did_print = True
355 desc = mod.__doc__.strip()
356 desc = desc.split('\n')[0]
357 start_str = " Module " + mod.__name__ + ": "
358 print start_str + _space_to(22, start_str) + desc
Dan Talayco2c0dba32010-03-06 22:47:06 -0800359 for test in config["all_tests"][mod]:
Dan Talayco79f36082010-03-11 16:53:53 -0800360 desc = eval('mod.' + test + '.__doc__.strip()')
361 desc = desc.split('\n')[0]
362 start_str = " " + test + ":"
363 print start_str + _space_to(22, start_str) + desc
364 print
365 if not did_print:
366 print "No tests found for " + config["test_spec"]
Dan Talayco2c0dba32010-03-06 22:47:06 -0800367 sys.exit(0)
368
369logging_setup(config)
370logging.info("++++++++ " + time.asctime() + " ++++++++")
371
372# Generate the test suite
373#@todo Decide if multiple suites are ever needed
374suite = unittest.TestSuite()
375
376if config["test_spec"] == "all":
377 for mod in config["all_tests"].keys():
378 for test in config["all_tests"][mod]:
379 add_test(suite, mod, test)
380
381else:
382 for ts_entry in config["test_spec"].split(","):
383 parts = ts_entry.split(".")
384
385 if len(parts) == 1: # Either a module or test name
386 if ts_entry in config["mod_name_map"].keys():
387 mod = config["mod_name_map"][ts_entry]
388 for test in config["all_tests"][mod]:
389 add_test(suite, mod, test)
390 else: # Search for matching tests
391 test_found = False
392 for mod in config["all_tests"].keys():
393 if ts_entry in config["all_tests"][mod]:
394 add_test(suite, mod, ts_entry)
395 test_found = True
396 if not test_found:
397 die("Could not find module or test: " + ts_entry)
398
399 elif len(parts) == 2: # module.test
400 if parts[0] not in config["mod_name_map"]:
401 die("Unknown module in test spec: " + ts_entry)
402 mod = config["mod_name_map"][parts[0]]
403 if parts[1] in config["all_tests"][mod]:
404 add_test(suite, mod, parts[1])
405 else:
406 die("No known test matches: " + ts_entry)
407
408 else:
409 die("Bad test spec: " + ts_entry)
410
411# Check if platform specified
Dan Talayco48370102010-03-03 15:17:33 -0800412if config["platform"]:
413 _imp_string = "from " + config["platform"] + " import *"
Dan Talayco2c0dba32010-03-06 22:47:06 -0800414 logging.info("Importing platform: " + _imp_string)
Dan Talayco48370102010-03-03 15:17:33 -0800415 try:
416 exec(_imp_string)
417 except:
418 logging.warn("Failed to import " + config["platform"] + " file")
419
420try:
421 platform_config_update(config)
422except:
423 logging.warn("Could not run platform host configuration")
424
425if not config["port_map"]:
426 # Try to set up default port mapping if not done by platform
427 config["port_map"] = default_port_map_setup(config)
428
429if not config["port_map"]:
Dan Talayco2c0dba32010-03-06 22:47:06 -0800430 die("Interface port map is not defined. Exiting")
Dan Talayco48370102010-03-03 15:17:33 -0800431
432logging.debug("Configuration: " + str(config))
433logging.info("OF port map: " + str(config["port_map"]))
434
435# Init the test sets
Dan Talayco2c0dba32010-03-06 22:47:06 -0800436for (modname,mod) in config["mod_name_map"].items():
437 try:
438 mod.test_set_init(config)
439 except:
440 logging.warning("Could not run test_set_init for " + modname)
Dan Talayco48370102010-03-03 15:17:33 -0800441
Dan Talayco2c0dba32010-03-06 22:47:06 -0800442if config["dbg_level"] == logging.CRITICAL:
443 _verb = 0
444elif config["dbg_level"] >= logging.WARNING:
445 _verb = 1
446else:
447 _verb = 2
Dan Talayco48370102010-03-03 15:17:33 -0800448
Brandon Heller446c1432010-04-01 12:43:27 -0700449if os.getuid() != 0:
450 print "ERROR: Super-user privileges required. Please re-run with " \
451 "sudo or as root."
452 exit(1)
453
Dan Talayco2c0dba32010-03-06 22:47:06 -0800454logging.info("*** TEST RUN START: " + time.asctime())
455unittest.TextTestRunner(verbosity=_verb).run(suite)
456logging.info("*** TEST RUN END : " + time.asctime())
Dan Talayco48370102010-03-03 15:17:33 -0800457