blob: cdf5af9af5373594d2d1c60a45b942d40e63168f [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
11ygenerally called config. The keys have the following
12significance.
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
98@todo Generate test catalog; support list, selection and grouping
99
100Proposed test case setup:
101 Files in this or sub directories (or later, directory specified on
102command line) that contain a function test_set_init are considered test
103files.
104 The function test_set_init examines the test_spec config variable
105and generates a suite of tests.
106 Support a command line option --test_mod so that all tests in that
107module will be run.
108 Support all to specify all tests from the module.
109
Dan Talayco48370102010-03-03 15:17:33 -0800110"""
111
112import sys
113from optparse import OptionParser
Dan Talayco2c0dba32010-03-06 22:47:06 -0800114from subprocess import Popen,PIPE
Dan Talayco48370102010-03-03 15:17:33 -0800115import logging
116import unittest
Dan Talayco2c0dba32010-03-06 22:47:06 -0800117import time
Dan Talayco48370102010-03-03 15:17:33 -0800118
119##@var DEBUG_LEVELS
120# Map from strings to debugging levels
121DEBUG_LEVELS = {
122 'debug' : logging.DEBUG,
123 'verbose' : logging.DEBUG,
124 'info' : logging.INFO,
125 'warning' : logging.WARNING,
126 'warn' : logging.WARNING,
127 'error' : logging.ERROR,
128 'critical' : logging.CRITICAL
129}
130
131_debug_default = "warning"
132_debug_level_default = DEBUG_LEVELS[_debug_default]
133
134##@var config_default
135# The default configuration dictionary for OFT
Dan Talayco2c0dba32010-03-06 22:47:06 -0800136#@todo Set up a dict of config params so easier to manage
Dan Talayco48370102010-03-03 15:17:33 -0800137config_default = {
138 "platform" : "local",
139 "controller_host" : "127.0.0.1",
140 "controller_port" : 6633,
141 "port_count" : 4,
142 "base_of_port" : 1,
143 "base_if_index" : 1,
Dan Talayco2c0dba32010-03-06 22:47:06 -0800144 "test_spec" : "all",
145 "test_dir" : ".",
Dan Talayco48370102010-03-03 15:17:33 -0800146 "log_file" : "oft.log",
Dan Talayco2c0dba32010-03-06 22:47:06 -0800147 "list" : False,
Dan Talayco48370102010-03-03 15:17:33 -0800148 "debug" : _debug_default,
149 "dbg_level" : _debug_level_default,
150 "port_map" : {}
151}
152
153# Map options to config structure
154def config_get(opts):
155 "Convert options class to OFT configuration dictionary"
156 cfg = config_default.copy()
Dan Talayco2c0dba32010-03-06 22:47:06 -0800157 for key in cfg.keys():
158 cfg[key] = eval("opts." + key)
159
160 # Special case checks
Dan Talayco48370102010-03-03 15:17:33 -0800161 if opts.debug not in DEBUG_LEVELS.keys():
162 print "Warning: Bad value specified for debug level; using default"
163 opts.debug = _debug_default
Dan Talayco48370102010-03-03 15:17:33 -0800164 cfg["dbg_level"] = DEBUG_LEVELS[cfg["debug"]]
Dan Talayco2c0dba32010-03-06 22:47:06 -0800165
Dan Talayco48370102010-03-03 15:17:33 -0800166 return cfg
167
168def config_setup(cfg_dflt):
169 """
170 Set up the configuration including parsing the arguments
171
172 @param cfg_dflt The default configuration dictionary
173 @return A pair (config, args) where config is an config
174 object and args is any additional arguments from the command line
175 """
176
177 parser = OptionParser(version="%prog 0.1")
178
Dan Talayco2c0dba32010-03-06 22:47:06 -0800179 #@todo parse port map as option?
Dan Talayco48370102010-03-03 15:17:33 -0800180 # Set up default values
Dan Talayco2c0dba32010-03-06 22:47:06 -0800181 for key in cfg_dflt.keys():
182 eval("parser.set_defaults("+key+"=cfg_dflt['"+key+"'])")
Dan Talayco48370102010-03-03 15:17:33 -0800183
Dan Talayco2c0dba32010-03-06 22:47:06 -0800184 #@todo Add options via dictionary
Dan Talayco48370102010-03-03 15:17:33 -0800185 plat_help = """Set the platform type. Valid values include:
186 local: User space virtual ethernet pair setup
187 remote: Remote embedded Broadcom based switch
Dan Talayco673e0852010-03-06 23:09:23 -0800188 Create a new_plat.py file and use --platform=new_plat on the command line
Dan Talayco48370102010-03-03 15:17:33 -0800189 """
190 parser.add_option("-P", "--platform", help=plat_help)
191 parser.add_option("-H", "--host", dest="controller_host",
192 help="The IP/name of the test controller host")
193 parser.add_option("-p", "--port", dest="controller_port",
194 type="int", help="Port number of the test controller")
Dan Talayco673e0852010-03-06 23:09:23 -0800195 test_list_help = """Indicate tests to run. Valid entries are "all" (the
196 default) or a comma separated list of:
197 module Run all tests in the named module
198 testcase Run tests in all modules with the name testcase
199 module.testcase Run the specific test case
200 """
201 parser.add_option("--test-spec", "--test-list", help=test_list_help)
Dan Talayco48370102010-03-03 15:17:33 -0800202 parser.add_option("--log-file",
203 help="Name of log file, empty string to log to console")
204 parser.add_option("--debug",
205 help="Debug lvl: debug, info, warning, error, critical")
Dan Talayco2c0dba32010-03-06 22:47:06 -0800206 parser.add_option("--port-count",
Dan Talayco48370102010-03-03 15:17:33 -0800207 help="Number of ports to use (optional)")
Dan Talayco2c0dba32010-03-06 22:47:06 -0800208 parser.add_option("--base-of-port",
Dan Talayco48370102010-03-03 15:17:33 -0800209 help="Base OpenFlow port number (optional)")
Dan Talayco2c0dba32010-03-06 22:47:06 -0800210 parser.add_option("--base-if-index",
211 help="Base interface index number (optional)")
212 parser.add_option("--list", action="store_true",
Dan Talayco48370102010-03-03 15:17:33 -0800213 help="Base interface index number (optional)")
214 # Might need this if other parsers want command line
215 # parser.allow_interspersed_args = False
216 (options, args) = parser.parse_args()
217
218 config = config_get(options)
219
220 return (config, args)
221
222def logging_setup(config):
223 """
224 Set up logging based on config
225 """
226 _format = "%(asctime)s %(name)-10s: %(levelname)-8s: %(message)s"
227 _datefmt = "%H:%M:%S"
Dan Talayco88fc8802010-03-07 11:37:52 -0800228 logging.basicConfig(filename=config["log_file"],
229 level=config["dbg_level"],
230 format=_format, datefmt=_datefmt)
Dan Talayco48370102010-03-03 15:17:33 -0800231
232def default_port_map_setup(config):
233 """
234 Setup the OF port mapping based on config
235 @param config The OFT configuration structure
236 @return Port map dictionary
237 """
238 if (config["base_of_port"] is None) or not config["port_count"]:
239 return None
240 port_map = {}
241 if config["platform"] == "local":
242 # For local, use every other veth port
243 for idx in range(config["port_count"]):
244 port_map[config["base_of_port"] + idx] = "veth" + \
245 str(config["base_if_index"] + (2 * idx))
246 elif config["platform"] == "remote":
247 # For remote, use eth ports
248 for idx in range(config["port_count"]):
249 port_map[config["base_of_port"] + idx] = "eth" + \
250 str(config["base_if_index"] + idx)
251 else:
252 return None
253
254 logging.info("Built default port map")
255 return port_map
256
Dan Talayco2c0dba32010-03-06 22:47:06 -0800257def test_list_generate(config):
258 """Generate the list of all known tests indexed by module name
259
260 Conventions: Test files must implement the function test_set_init
261
262 Test cases are classes that implement testRun
263
264 @param config The oft configuration dictionary
265 @returns An array of triples (mod-name, module, [tests]) where
266 mod-name is the string (filename) of the module, module is the
267 value returned from __import__'ing the module and [tests] is an
268 array of strings giving the test cases from the module.
269 """
270
271 # Find and import test files
272 p1 = Popen(["find", config["test_dir"], "-type","f"], stdout = PIPE)
273 p2 = Popen(["xargs", "grep", "-l", "-e", "^def test_set_init"],
274 stdin=p1.stdout, stdout=PIPE)
275
276 all_tests = {}
277 mod_name_map = {}
278 # There's an extra empty entry at the end of the list
279 filelist = p2.communicate()[0].split("\n")[:-1]
280 for file in filelist:
281 modfile = file.lstrip('./')[:-3]
282
283 try:
284 mod = __import__(modfile)
285 except:
286 logging.warning("Could not import file " + file)
287 continue
288 mod_name_map[modfile] = mod
289 added_fn = False
290 for fn in dir(mod):
291 if 'runTest' in dir(eval("mod." + fn)):
292 if not added_fn:
293 mod_name_map[modfile] = mod
294 all_tests[mod] = []
295 added_fn = True
296 all_tests[mod].append(fn)
297 config["all_tests"] = all_tests
298 config["mod_name_map"] = mod_name_map
299
300def die(msg, exit_val=1):
301 print msg
302 logging.critical(msg)
303 sys.exit(exit_val)
304
305def add_test(suite, mod, name):
306 logging.info("Adding test " + mod.__name__ + "." + name)
307 suite.addTest(eval("mod." + name)())
308
Dan Talayco48370102010-03-03 15:17:33 -0800309#
310# Main script
311#
312
313# Get configuration, set up logging, import platform from file
314(config, args) = config_setup(config_default)
Dan Talayco48370102010-03-03 15:17:33 -0800315
Dan Talayco2c0dba32010-03-06 22:47:06 -0800316test_list_generate(config)
317
318# Check if test list is requested; display and exit if so
319if config["list"]:
320 print "\nTest List:"
321 for mod in config["all_tests"].keys():
322 print " Module: " + mod.__name__
323 for test in config["all_tests"][mod]:
324 print " " + test
325 sys.exit(0)
326
327logging_setup(config)
328logging.info("++++++++ " + time.asctime() + " ++++++++")
329
330# Generate the test suite
331#@todo Decide if multiple suites are ever needed
332suite = unittest.TestSuite()
333
334if config["test_spec"] == "all":
335 for mod in config["all_tests"].keys():
336 for test in config["all_tests"][mod]:
337 add_test(suite, mod, test)
338
339else:
340 for ts_entry in config["test_spec"].split(","):
341 parts = ts_entry.split(".")
342
343 if len(parts) == 1: # Either a module or test name
344 if ts_entry in config["mod_name_map"].keys():
345 mod = config["mod_name_map"][ts_entry]
346 for test in config["all_tests"][mod]:
347 add_test(suite, mod, test)
348 else: # Search for matching tests
349 test_found = False
350 for mod in config["all_tests"].keys():
351 if ts_entry in config["all_tests"][mod]:
352 add_test(suite, mod, ts_entry)
353 test_found = True
354 if not test_found:
355 die("Could not find module or test: " + ts_entry)
356
357 elif len(parts) == 2: # module.test
358 if parts[0] not in config["mod_name_map"]:
359 die("Unknown module in test spec: " + ts_entry)
360 mod = config["mod_name_map"][parts[0]]
361 if parts[1] in config["all_tests"][mod]:
362 add_test(suite, mod, parts[1])
363 else:
364 die("No known test matches: " + ts_entry)
365
366 else:
367 die("Bad test spec: " + ts_entry)
368
369# Check if platform specified
Dan Talayco48370102010-03-03 15:17:33 -0800370if config["platform"]:
371 _imp_string = "from " + config["platform"] + " import *"
Dan Talayco2c0dba32010-03-06 22:47:06 -0800372 logging.info("Importing platform: " + _imp_string)
Dan Talayco48370102010-03-03 15:17:33 -0800373 try:
374 exec(_imp_string)
375 except:
376 logging.warn("Failed to import " + config["platform"] + " file")
377
378try:
379 platform_config_update(config)
380except:
381 logging.warn("Could not run platform host configuration")
382
383if not config["port_map"]:
384 # Try to set up default port mapping if not done by platform
385 config["port_map"] = default_port_map_setup(config)
386
387if not config["port_map"]:
Dan Talayco2c0dba32010-03-06 22:47:06 -0800388 die("Interface port map is not defined. Exiting")
Dan Talayco48370102010-03-03 15:17:33 -0800389
390logging.debug("Configuration: " + str(config))
391logging.info("OF port map: " + str(config["port_map"]))
392
393# Init the test sets
Dan Talayco2c0dba32010-03-06 22:47:06 -0800394for (modname,mod) in config["mod_name_map"].items():
395 try:
396 mod.test_set_init(config)
397 except:
398 logging.warning("Could not run test_set_init for " + modname)
Dan Talayco48370102010-03-03 15:17:33 -0800399
Dan Talayco2c0dba32010-03-06 22:47:06 -0800400if config["dbg_level"] == logging.CRITICAL:
401 _verb = 0
402elif config["dbg_level"] >= logging.WARNING:
403 _verb = 1
404else:
405 _verb = 2
Dan Talayco48370102010-03-03 15:17:33 -0800406
Dan Talayco2c0dba32010-03-06 22:47:06 -0800407logging.info("*** TEST RUN START: " + time.asctime())
408unittest.TextTestRunner(verbosity=_verb).run(suite)
409logging.info("*** TEST RUN END : " + time.asctime())
Dan Talayco48370102010-03-03 15:17:33 -0800410