blob: a4fb8f738422420fb7e2ec9440e9c948ba085505 [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
77@todo Determine and implement conventions for test_spec.
78
79The current model for test sets is basic.py. The current convention is
80that the test set should implement a function test_set_init which takes
81an oft configuration dictionary and returns a unittest.TestSuite object.
82Future test sets should do the same thing.
83
Dan Talayco52f64442010-03-03 15:32:41 -080084Default setup:
85
86The default setup runs locally using veth pairs. To exercise this,
87checkout and build an openflow userspace datapath. Then start it on
88the local host:
Dan Talayco2c0dba32010-03-06 22:47:06 -080089<pre>
Dan Talayco52f64442010-03-03 15:32:41 -080090 sudo ~/openflow/regress/bin/veth_setup.pl
91 sudo ofdatapath -i veth0,veth2,veth4,veth6 punix:/tmp/ofd &
92 sudo ofprotocol unix:/tmp/ofd tcp:127.0.0.1 --fail=closed --max-backoff=1 &
93
94Next, run oft:
95 sudo ./oft --debug=info
Dan Talayco2c0dba32010-03-06 22:47:06 -080096</pre>
Dan Talayco52f64442010-03-03 15:32:41 -080097
98Examine oft.log if things don't work.
Dan Talayco2c0dba32010-03-06 22:47:06 -080099
100@todo Generate test catalog; support list, selection and grouping
101
102Proposed test case setup:
103 Files in this or sub directories (or later, directory specified on
104command line) that contain a function test_set_init are considered test
105files.
106 The function test_set_init examines the test_spec config variable
107and generates a suite of tests.
108 Support a command line option --test_mod so that all tests in that
109module will be run.
110 Support all to specify all tests from the module.
111
Dan Talayco48370102010-03-03 15:17:33 -0800112"""
113
114import sys
115from optparse import OptionParser
Dan Talayco2c0dba32010-03-06 22:47:06 -0800116from subprocess import Popen,PIPE
Dan Talayco48370102010-03-03 15:17:33 -0800117import logging
118import unittest
Dan Talayco2c0dba32010-03-06 22:47:06 -0800119import time
Dan Talayco48370102010-03-03 15:17:33 -0800120
121##@var DEBUG_LEVELS
122# Map from strings to debugging levels
123DEBUG_LEVELS = {
124 'debug' : logging.DEBUG,
125 'verbose' : logging.DEBUG,
126 'info' : logging.INFO,
127 'warning' : logging.WARNING,
128 'warn' : logging.WARNING,
129 'error' : logging.ERROR,
130 'critical' : logging.CRITICAL
131}
132
133_debug_default = "warning"
134_debug_level_default = DEBUG_LEVELS[_debug_default]
135
136##@var config_default
137# The default configuration dictionary for OFT
Dan Talayco2c0dba32010-03-06 22:47:06 -0800138#@todo Set up a dict of config params so easier to manage
Dan Talayco48370102010-03-03 15:17:33 -0800139config_default = {
140 "platform" : "local",
141 "controller_host" : "127.0.0.1",
142 "controller_port" : 6633,
143 "port_count" : 4,
144 "base_of_port" : 1,
145 "base_if_index" : 1,
Dan Talayco2c0dba32010-03-06 22:47:06 -0800146 "test_spec" : "all",
147 "test_dir" : ".",
Dan Talayco48370102010-03-03 15:17:33 -0800148 "log_file" : "oft.log",
Dan Talayco2c0dba32010-03-06 22:47:06 -0800149 "list" : False,
Dan Talayco48370102010-03-03 15:17:33 -0800150 "debug" : _debug_default,
151 "dbg_level" : _debug_level_default,
152 "port_map" : {}
153}
154
155# Map options to config structure
156def config_get(opts):
157 "Convert options class to OFT configuration dictionary"
158 cfg = config_default.copy()
Dan Talayco2c0dba32010-03-06 22:47:06 -0800159 for key in cfg.keys():
160 cfg[key] = eval("opts." + key)
161
162 # Special case checks
Dan Talayco48370102010-03-03 15:17:33 -0800163 if opts.debug not in DEBUG_LEVELS.keys():
164 print "Warning: Bad value specified for debug level; using default"
165 opts.debug = _debug_default
Dan Talayco48370102010-03-03 15:17:33 -0800166 cfg["dbg_level"] = DEBUG_LEVELS[cfg["debug"]]
Dan Talayco2c0dba32010-03-06 22:47:06 -0800167
Dan Talayco48370102010-03-03 15:17:33 -0800168 return cfg
169
170def config_setup(cfg_dflt):
171 """
172 Set up the configuration including parsing the arguments
173
174 @param cfg_dflt The default configuration dictionary
175 @return A pair (config, args) where config is an config
176 object and args is any additional arguments from the command line
177 """
178
179 parser = OptionParser(version="%prog 0.1")
180
Dan Talayco2c0dba32010-03-06 22:47:06 -0800181 #@todo parse port map as option?
Dan Talayco48370102010-03-03 15:17:33 -0800182 # Set up default values
Dan Talayco2c0dba32010-03-06 22:47:06 -0800183 for key in cfg_dflt.keys():
184 eval("parser.set_defaults("+key+"=cfg_dflt['"+key+"'])")
Dan Talayco48370102010-03-03 15:17:33 -0800185
Dan Talayco2c0dba32010-03-06 22:47:06 -0800186 #@todo Add options via dictionary
Dan Talayco48370102010-03-03 15:17:33 -0800187 plat_help = """Set the platform type. Valid values include:
188 local: User space virtual ethernet pair setup
189 remote: Remote embedded Broadcom based switch
Dan Talayco673e0852010-03-06 23:09:23 -0800190 Create a new_plat.py file and use --platform=new_plat on the command line
Dan Talayco48370102010-03-03 15:17:33 -0800191 """
192 parser.add_option("-P", "--platform", help=plat_help)
193 parser.add_option("-H", "--host", dest="controller_host",
194 help="The IP/name of the test controller host")
195 parser.add_option("-p", "--port", dest="controller_port",
196 type="int", help="Port number of the test controller")
Dan Talayco673e0852010-03-06 23:09:23 -0800197 test_list_help = """Indicate tests to run. Valid entries are "all" (the
198 default) or a comma separated list of:
199 module Run all tests in the named module
200 testcase Run tests in all modules with the name testcase
201 module.testcase Run the specific test case
202 """
203 parser.add_option("--test-spec", "--test-list", help=test_list_help)
Dan Talayco48370102010-03-03 15:17:33 -0800204 parser.add_option("--log-file",
205 help="Name of log file, empty string to log to console")
206 parser.add_option("--debug",
207 help="Debug lvl: debug, info, warning, error, critical")
Dan Talayco2c0dba32010-03-06 22:47:06 -0800208 parser.add_option("--port-count",
Dan Talayco48370102010-03-03 15:17:33 -0800209 help="Number of ports to use (optional)")
Dan Talayco2c0dba32010-03-06 22:47:06 -0800210 parser.add_option("--base-of-port",
Dan Talayco48370102010-03-03 15:17:33 -0800211 help="Base OpenFlow port number (optional)")
Dan Talayco2c0dba32010-03-06 22:47:06 -0800212 parser.add_option("--base-if-index",
213 help="Base interface index number (optional)")
214 parser.add_option("--list", action="store_true",
Dan Talayco48370102010-03-03 15:17:33 -0800215 help="Base interface index number (optional)")
216 # Might need this if other parsers want command line
217 # parser.allow_interspersed_args = False
218 (options, args) = parser.parse_args()
219
220 config = config_get(options)
221
222 return (config, args)
223
224def logging_setup(config):
225 """
226 Set up logging based on config
227 """
228 _format = "%(asctime)s %(name)-10s: %(levelname)-8s: %(message)s"
229 _datefmt = "%H:%M:%S"
230 if config["log_file"]:
231 logging.basicConfig(filename=config["log_file"],
232 level=config["dbg_level"],
233 format=_format, datefmt=_datefmt)
234 #@todo Handle "no log file"
235
236def default_port_map_setup(config):
237 """
238 Setup the OF port mapping based on config
239 @param config The OFT configuration structure
240 @return Port map dictionary
241 """
242 if (config["base_of_port"] is None) or not config["port_count"]:
243 return None
244 port_map = {}
245 if config["platform"] == "local":
246 # For local, use every other veth port
247 for idx in range(config["port_count"]):
248 port_map[config["base_of_port"] + idx] = "veth" + \
249 str(config["base_if_index"] + (2 * idx))
250 elif config["platform"] == "remote":
251 # For remote, use eth ports
252 for idx in range(config["port_count"]):
253 port_map[config["base_of_port"] + idx] = "eth" + \
254 str(config["base_if_index"] + idx)
255 else:
256 return None
257
258 logging.info("Built default port map")
259 return port_map
260
Dan Talayco2c0dba32010-03-06 22:47:06 -0800261def test_list_generate(config):
262 """Generate the list of all known tests indexed by module name
263
264 Conventions: Test files must implement the function test_set_init
265
266 Test cases are classes that implement testRun
267
268 @param config The oft configuration dictionary
269 @returns An array of triples (mod-name, module, [tests]) where
270 mod-name is the string (filename) of the module, module is the
271 value returned from __import__'ing the module and [tests] is an
272 array of strings giving the test cases from the module.
273 """
274
275 # Find and import test files
276 p1 = Popen(["find", config["test_dir"], "-type","f"], stdout = PIPE)
277 p2 = Popen(["xargs", "grep", "-l", "-e", "^def test_set_init"],
278 stdin=p1.stdout, stdout=PIPE)
279
280 all_tests = {}
281 mod_name_map = {}
282 # There's an extra empty entry at the end of the list
283 filelist = p2.communicate()[0].split("\n")[:-1]
284 for file in filelist:
285 modfile = file.lstrip('./')[:-3]
286
287 try:
288 mod = __import__(modfile)
289 except:
290 logging.warning("Could not import file " + file)
291 continue
292 mod_name_map[modfile] = mod
293 added_fn = False
294 for fn in dir(mod):
295 if 'runTest' in dir(eval("mod." + fn)):
296 if not added_fn:
297 mod_name_map[modfile] = mod
298 all_tests[mod] = []
299 added_fn = True
300 all_tests[mod].append(fn)
301 config["all_tests"] = all_tests
302 config["mod_name_map"] = mod_name_map
303
304def die(msg, exit_val=1):
305 print msg
306 logging.critical(msg)
307 sys.exit(exit_val)
308
309def add_test(suite, mod, name):
310 logging.info("Adding test " + mod.__name__ + "." + name)
311 suite.addTest(eval("mod." + name)())
312
Dan Talayco48370102010-03-03 15:17:33 -0800313#
314# Main script
315#
316
317# Get configuration, set up logging, import platform from file
318(config, args) = config_setup(config_default)
Dan Talayco48370102010-03-03 15:17:33 -0800319
Dan Talayco2c0dba32010-03-06 22:47:06 -0800320test_list_generate(config)
321
322# Check if test list is requested; display and exit if so
323if config["list"]:
324 print "\nTest List:"
325 for mod in config["all_tests"].keys():
326 print " Module: " + mod.__name__
327 for test in config["all_tests"][mod]:
328 print " " + test
329 sys.exit(0)
330
331logging_setup(config)
332logging.info("++++++++ " + time.asctime() + " ++++++++")
333
334# Generate the test suite
335#@todo Decide if multiple suites are ever needed
336suite = unittest.TestSuite()
337
338if config["test_spec"] == "all":
339 for mod in config["all_tests"].keys():
340 for test in config["all_tests"][mod]:
341 add_test(suite, mod, test)
342
343else:
344 for ts_entry in config["test_spec"].split(","):
345 parts = ts_entry.split(".")
346
347 if len(parts) == 1: # Either a module or test name
348 if ts_entry in config["mod_name_map"].keys():
349 mod = config["mod_name_map"][ts_entry]
350 for test in config["all_tests"][mod]:
351 add_test(suite, mod, test)
352 else: # Search for matching tests
353 test_found = False
354 for mod in config["all_tests"].keys():
355 if ts_entry in config["all_tests"][mod]:
356 add_test(suite, mod, ts_entry)
357 test_found = True
358 if not test_found:
359 die("Could not find module or test: " + ts_entry)
360
361 elif len(parts) == 2: # module.test
362 if parts[0] not in config["mod_name_map"]:
363 die("Unknown module in test spec: " + ts_entry)
364 mod = config["mod_name_map"][parts[0]]
365 if parts[1] in config["all_tests"][mod]:
366 add_test(suite, mod, parts[1])
367 else:
368 die("No known test matches: " + ts_entry)
369
370 else:
371 die("Bad test spec: " + ts_entry)
372
373# Check if platform specified
Dan Talayco48370102010-03-03 15:17:33 -0800374if config["platform"]:
375 _imp_string = "from " + config["platform"] + " import *"
Dan Talayco2c0dba32010-03-06 22:47:06 -0800376 logging.info("Importing platform: " + _imp_string)
Dan Talayco48370102010-03-03 15:17:33 -0800377 try:
378 exec(_imp_string)
379 except:
380 logging.warn("Failed to import " + config["platform"] + " file")
381
382try:
383 platform_config_update(config)
384except:
385 logging.warn("Could not run platform host configuration")
386
387if not config["port_map"]:
388 # Try to set up default port mapping if not done by platform
389 config["port_map"] = default_port_map_setup(config)
390
391if not config["port_map"]:
Dan Talayco2c0dba32010-03-06 22:47:06 -0800392 die("Interface port map is not defined. Exiting")
Dan Talayco48370102010-03-03 15:17:33 -0800393
394logging.debug("Configuration: " + str(config))
395logging.info("OF port map: " + str(config["port_map"]))
396
397# Init the test sets
Dan Talayco2c0dba32010-03-06 22:47:06 -0800398for (modname,mod) in config["mod_name_map"].items():
399 try:
400 mod.test_set_init(config)
401 except:
402 logging.warning("Could not run test_set_init for " + modname)
Dan Talayco48370102010-03-03 15:17:33 -0800403
Dan Talayco2c0dba32010-03-06 22:47:06 -0800404if config["dbg_level"] == logging.CRITICAL:
405 _verb = 0
406elif config["dbg_level"] >= logging.WARNING:
407 _verb = 1
408else:
409 _verb = 2
Dan Talayco48370102010-03-03 15:17:33 -0800410
Dan Talayco2c0dba32010-03-06 22:47:06 -0800411logging.info("*** TEST RUN START: " + time.asctime())
412unittest.TextTestRunner(verbosity=_verb).run(suite)
413logging.info("*** TEST RUN END : " + time.asctime())
Dan Talayco48370102010-03-03 15:17:33 -0800414