blob: 1ace47dc5bf0cd788e10d37fb1917478a0ea0379 [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
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
Dan Talayco48370102010-03-03 15:17:33 -0800119
120##@var DEBUG_LEVELS
121# Map from strings to debugging levels
122DEBUG_LEVELS = {
123 'debug' : logging.DEBUG,
124 'verbose' : logging.DEBUG,
125 'info' : logging.INFO,
126 'warning' : logging.WARNING,
127 'warn' : logging.WARNING,
128 'error' : logging.ERROR,
129 'critical' : logging.CRITICAL
130}
131
132_debug_default = "warning"
133_debug_level_default = DEBUG_LEVELS[_debug_default]
134
135##@var config_default
136# The default configuration dictionary for OFT
137config_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
Dan Talayco1a88c122010-03-07 22:00:20 -0800153#@todo Set up a dict of config params so easier to manage:
154# <param> <cmdline flags> <default value> <help> <optional parser>
155
Dan Talayco48370102010-03-03 15:17:33 -0800156# Map options to config structure
157def config_get(opts):
158 "Convert options class to OFT configuration dictionary"
159 cfg = config_default.copy()
Dan Talayco2c0dba32010-03-06 22:47:06 -0800160 for key in cfg.keys():
161 cfg[key] = eval("opts." + key)
162
163 # Special case checks
Dan Talayco48370102010-03-03 15:17:33 -0800164 if opts.debug not in DEBUG_LEVELS.keys():
165 print "Warning: Bad value specified for debug level; using default"
166 opts.debug = _debug_default
Dan Talayco48370102010-03-03 15:17:33 -0800167 cfg["dbg_level"] = DEBUG_LEVELS[cfg["debug"]]
Dan Talayco2c0dba32010-03-06 22:47:06 -0800168
Dan Talayco48370102010-03-03 15:17:33 -0800169 return cfg
170
171def config_setup(cfg_dflt):
172 """
173 Set up the configuration including parsing the arguments
174
175 @param cfg_dflt The default configuration dictionary
176 @return A pair (config, args) where config is an config
177 object and args is any additional arguments from the command line
178 """
179
180 parser = OptionParser(version="%prog 0.1")
181
Dan Talayco2c0dba32010-03-06 22:47:06 -0800182 #@todo parse port map as option?
Dan Talayco48370102010-03-03 15:17:33 -0800183 # Set up default values
Dan Talayco2c0dba32010-03-06 22:47:06 -0800184 for key in cfg_dflt.keys():
185 eval("parser.set_defaults("+key+"=cfg_dflt['"+key+"'])")
Dan Talayco48370102010-03-03 15:17:33 -0800186
Dan Talayco2c0dba32010-03-06 22:47:06 -0800187 #@todo Add options via dictionary
Dan Talayco48370102010-03-03 15:17:33 -0800188 plat_help = """Set the platform type. Valid values include:
189 local: User space virtual ethernet pair setup
190 remote: Remote embedded Broadcom based switch
Dan Talayco673e0852010-03-06 23:09:23 -0800191 Create a new_plat.py file and use --platform=new_plat on the command line
Dan Talayco48370102010-03-03 15:17:33 -0800192 """
193 parser.add_option("-P", "--platform", help=plat_help)
194 parser.add_option("-H", "--host", dest="controller_host",
195 help="The IP/name of the test controller host")
196 parser.add_option("-p", "--port", dest="controller_port",
197 type="int", help="Port number of the test controller")
Dan Talayco673e0852010-03-06 23:09:23 -0800198 test_list_help = """Indicate tests to run. Valid entries are "all" (the
199 default) or a comma separated list of:
200 module Run all tests in the named module
201 testcase Run tests in all modules with the name testcase
202 module.testcase Run the specific test case
203 """
204 parser.add_option("--test-spec", "--test-list", help=test_list_help)
Dan Talayco48370102010-03-03 15:17:33 -0800205 parser.add_option("--log-file",
206 help="Name of log file, empty string to log to console")
207 parser.add_option("--debug",
208 help="Debug lvl: debug, info, warning, error, critical")
Dan Talayco2c0dba32010-03-06 22:47:06 -0800209 parser.add_option("--port-count",
Dan Talayco48370102010-03-03 15:17:33 -0800210 help="Number of ports to use (optional)")
Dan Talayco2c0dba32010-03-06 22:47:06 -0800211 parser.add_option("--base-of-port",
Dan Talayco48370102010-03-03 15:17:33 -0800212 help="Base OpenFlow port number (optional)")
Dan Talayco2c0dba32010-03-06 22:47:06 -0800213 parser.add_option("--base-if-index",
214 help="Base interface index number (optional)")
215 parser.add_option("--list", action="store_true",
Dan Talayco48370102010-03-03 15:17:33 -0800216 help="Base interface index number (optional)")
217 # Might need this if other parsers want command line
218 # parser.allow_interspersed_args = False
219 (options, args) = parser.parse_args()
220
221 config = config_get(options)
222
223 return (config, args)
224
225def logging_setup(config):
226 """
227 Set up logging based on config
228 """
229 _format = "%(asctime)s %(name)-10s: %(levelname)-8s: %(message)s"
230 _datefmt = "%H:%M:%S"
Dan Talayco88fc8802010-03-07 11:37:52 -0800231 logging.basicConfig(filename=config["log_file"],
232 level=config["dbg_level"],
233 format=_format, datefmt=_datefmt)
Dan Talayco48370102010-03-03 15:17:33 -0800234
235def default_port_map_setup(config):
236 """
237 Setup the OF port mapping based on config
238 @param config The OFT configuration structure
239 @return Port map dictionary
240 """
241 if (config["base_of_port"] is None) or not config["port_count"]:
242 return None
243 port_map = {}
244 if config["platform"] == "local":
245 # For local, use every other veth port
246 for idx in range(config["port_count"]):
247 port_map[config["base_of_port"] + idx] = "veth" + \
248 str(config["base_if_index"] + (2 * idx))
249 elif config["platform"] == "remote":
250 # For remote, use eth ports
251 for idx in range(config["port_count"]):
252 port_map[config["base_of_port"] + idx] = "eth" + \
253 str(config["base_if_index"] + idx)
254 else:
255 return None
256
257 logging.info("Built default port map")
258 return port_map
259
Dan Talayco2c0dba32010-03-06 22:47:06 -0800260def test_list_generate(config):
261 """Generate the list of all known tests indexed by module name
262
263 Conventions: Test files must implement the function test_set_init
264
Dan Talayco1a88c122010-03-07 22:00:20 -0800265 Test cases are classes that implement runTest
Dan Talayco2c0dba32010-03-06 22:47:06 -0800266
267 @param config The oft configuration dictionary
268 @returns An array of triples (mod-name, module, [tests]) where
269 mod-name is the string (filename) of the module, module is the
270 value returned from __import__'ing the module and [tests] is an
271 array of strings giving the test cases from the module.
272 """
273
274 # Find and import test files
275 p1 = Popen(["find", config["test_dir"], "-type","f"], stdout = PIPE)
276 p2 = Popen(["xargs", "grep", "-l", "-e", "^def test_set_init"],
277 stdin=p1.stdout, stdout=PIPE)
278
279 all_tests = {}
280 mod_name_map = {}
281 # There's an extra empty entry at the end of the list
282 filelist = p2.communicate()[0].split("\n")[:-1]
283 for file in filelist:
284 modfile = file.lstrip('./')[:-3]
285
286 try:
287 mod = __import__(modfile)
288 except:
289 logging.warning("Could not import file " + file)
290 continue
291 mod_name_map[modfile] = mod
292 added_fn = False
293 for fn in dir(mod):
294 if 'runTest' in dir(eval("mod." + fn)):
295 if not added_fn:
296 mod_name_map[modfile] = mod
297 all_tests[mod] = []
298 added_fn = True
299 all_tests[mod].append(fn)
300 config["all_tests"] = all_tests
301 config["mod_name_map"] = mod_name_map
302
303def die(msg, exit_val=1):
304 print msg
305 logging.critical(msg)
306 sys.exit(exit_val)
307
308def add_test(suite, mod, name):
309 logging.info("Adding test " + mod.__name__ + "." + name)
310 suite.addTest(eval("mod." + name)())
311
Dan Talayco48370102010-03-03 15:17:33 -0800312#
313# Main script
314#
315
316# Get configuration, set up logging, import platform from file
317(config, args) = config_setup(config_default)
Dan Talayco48370102010-03-03 15:17:33 -0800318
Dan Talayco2c0dba32010-03-06 22:47:06 -0800319test_list_generate(config)
320
321# Check if test list is requested; display and exit if so
322if config["list"]:
323 print "\nTest List:"
324 for mod in config["all_tests"].keys():
325 print " Module: " + mod.__name__
326 for test in config["all_tests"][mod]:
327 print " " + test
328 sys.exit(0)
329
330logging_setup(config)
331logging.info("++++++++ " + time.asctime() + " ++++++++")
332
333# Generate the test suite
334#@todo Decide if multiple suites are ever needed
335suite = unittest.TestSuite()
336
337if config["test_spec"] == "all":
338 for mod in config["all_tests"].keys():
339 for test in config["all_tests"][mod]:
340 add_test(suite, mod, test)
341
342else:
343 for ts_entry in config["test_spec"].split(","):
344 parts = ts_entry.split(".")
345
346 if len(parts) == 1: # Either a module or test name
347 if ts_entry in config["mod_name_map"].keys():
348 mod = config["mod_name_map"][ts_entry]
349 for test in config["all_tests"][mod]:
350 add_test(suite, mod, test)
351 else: # Search for matching tests
352 test_found = False
353 for mod in config["all_tests"].keys():
354 if ts_entry in config["all_tests"][mod]:
355 add_test(suite, mod, ts_entry)
356 test_found = True
357 if not test_found:
358 die("Could not find module or test: " + ts_entry)
359
360 elif len(parts) == 2: # module.test
361 if parts[0] not in config["mod_name_map"]:
362 die("Unknown module in test spec: " + ts_entry)
363 mod = config["mod_name_map"][parts[0]]
364 if parts[1] in config["all_tests"][mod]:
365 add_test(suite, mod, parts[1])
366 else:
367 die("No known test matches: " + ts_entry)
368
369 else:
370 die("Bad test spec: " + ts_entry)
371
372# Check if platform specified
Dan Talayco48370102010-03-03 15:17:33 -0800373if config["platform"]:
374 _imp_string = "from " + config["platform"] + " import *"
Dan Talayco2c0dba32010-03-06 22:47:06 -0800375 logging.info("Importing platform: " + _imp_string)
Dan Talayco48370102010-03-03 15:17:33 -0800376 try:
377 exec(_imp_string)
378 except:
379 logging.warn("Failed to import " + config["platform"] + " file")
380
381try:
382 platform_config_update(config)
383except:
384 logging.warn("Could not run platform host configuration")
385
386if not config["port_map"]:
387 # Try to set up default port mapping if not done by platform
388 config["port_map"] = default_port_map_setup(config)
389
390if not config["port_map"]:
Dan Talayco2c0dba32010-03-06 22:47:06 -0800391 die("Interface port map is not defined. Exiting")
Dan Talayco48370102010-03-03 15:17:33 -0800392
393logging.debug("Configuration: " + str(config))
394logging.info("OF port map: " + str(config["port_map"]))
395
396# Init the test sets
Dan Talayco2c0dba32010-03-06 22:47:06 -0800397for (modname,mod) in config["mod_name_map"].items():
398 try:
399 mod.test_set_init(config)
400 except:
401 logging.warning("Could not run test_set_init for " + modname)
Dan Talayco48370102010-03-03 15:17:33 -0800402
Dan Talayco2c0dba32010-03-06 22:47:06 -0800403if config["dbg_level"] == logging.CRITICAL:
404 _verb = 0
405elif config["dbg_level"] >= logging.WARNING:
406 _verb = 1
407else:
408 _verb = 2
Dan Talayco48370102010-03-03 15:17:33 -0800409
Dan Talayco2c0dba32010-03-06 22:47:06 -0800410logging.info("*** TEST RUN START: " + time.asctime())
411unittest.TextTestRunner(verbosity=_verb).run(suite)
412logging.info("*** TEST RUN END : " + time.asctime())
Dan Talayco48370102010-03-03 15:17:33 -0800413