blob: 32c1b8566938e050d8ac8eb9b1d237c2f9a16e0f [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
190 """
191 parser.add_option("-P", "--platform", help=plat_help)
192 parser.add_option("-H", "--host", dest="controller_host",
193 help="The IP/name of the test controller host")
194 parser.add_option("-p", "--port", dest="controller_port",
195 type="int", help="Port number of the test controller")
196 parser.add_option("--test-spec", "--test-list",
197 help="Indicate tests to run (TBD)")
198 parser.add_option("--log-file",
199 help="Name of log file, empty string to log to console")
200 parser.add_option("--debug",
201 help="Debug lvl: debug, info, warning, error, critical")
Dan Talayco2c0dba32010-03-06 22:47:06 -0800202 parser.add_option("--port-count",
Dan Talayco48370102010-03-03 15:17:33 -0800203 help="Number of ports to use (optional)")
Dan Talayco2c0dba32010-03-06 22:47:06 -0800204 parser.add_option("--base-of-port",
Dan Talayco48370102010-03-03 15:17:33 -0800205 help="Base OpenFlow port number (optional)")
Dan Talayco2c0dba32010-03-06 22:47:06 -0800206 parser.add_option("--base-if-index",
207 help="Base interface index number (optional)")
208 parser.add_option("--list", action="store_true",
Dan Talayco48370102010-03-03 15:17:33 -0800209 help="Base interface index number (optional)")
210 # Might need this if other parsers want command line
211 # parser.allow_interspersed_args = False
212 (options, args) = parser.parse_args()
213
214 config = config_get(options)
215
216 return (config, args)
217
218def logging_setup(config):
219 """
220 Set up logging based on config
221 """
222 _format = "%(asctime)s %(name)-10s: %(levelname)-8s: %(message)s"
223 _datefmt = "%H:%M:%S"
224 if config["log_file"]:
225 logging.basicConfig(filename=config["log_file"],
226 level=config["dbg_level"],
227 format=_format, datefmt=_datefmt)
228 #@todo Handle "no log file"
229
230def default_port_map_setup(config):
231 """
232 Setup the OF port mapping based on config
233 @param config The OFT configuration structure
234 @return Port map dictionary
235 """
236 if (config["base_of_port"] is None) or not config["port_count"]:
237 return None
238 port_map = {}
239 if config["platform"] == "local":
240 # For local, use every other veth port
241 for idx in range(config["port_count"]):
242 port_map[config["base_of_port"] + idx] = "veth" + \
243 str(config["base_if_index"] + (2 * idx))
244 elif config["platform"] == "remote":
245 # For remote, use eth ports
246 for idx in range(config["port_count"]):
247 port_map[config["base_of_port"] + idx] = "eth" + \
248 str(config["base_if_index"] + idx)
249 else:
250 return None
251
252 logging.info("Built default port map")
253 return port_map
254
Dan Talayco2c0dba32010-03-06 22:47:06 -0800255def test_list_generate(config):
256 """Generate the list of all known tests indexed by module name
257
258 Conventions: Test files must implement the function test_set_init
259
260 Test cases are classes that implement testRun
261
262 @param config The oft configuration dictionary
263 @returns An array of triples (mod-name, module, [tests]) where
264 mod-name is the string (filename) of the module, module is the
265 value returned from __import__'ing the module and [tests] is an
266 array of strings giving the test cases from the module.
267 """
268
269 # Find and import test files
270 p1 = Popen(["find", config["test_dir"], "-type","f"], stdout = PIPE)
271 p2 = Popen(["xargs", "grep", "-l", "-e", "^def test_set_init"],
272 stdin=p1.stdout, stdout=PIPE)
273
274 all_tests = {}
275 mod_name_map = {}
276 # There's an extra empty entry at the end of the list
277 filelist = p2.communicate()[0].split("\n")[:-1]
278 for file in filelist:
279 modfile = file.lstrip('./')[:-3]
280
281 try:
282 mod = __import__(modfile)
283 except:
284 logging.warning("Could not import file " + file)
285 continue
286 mod_name_map[modfile] = mod
287 added_fn = False
288 for fn in dir(mod):
289 if 'runTest' in dir(eval("mod." + fn)):
290 if not added_fn:
291 mod_name_map[modfile] = mod
292 all_tests[mod] = []
293 added_fn = True
294 all_tests[mod].append(fn)
295 config["all_tests"] = all_tests
296 config["mod_name_map"] = mod_name_map
297
298def die(msg, exit_val=1):
299 print msg
300 logging.critical(msg)
301 sys.exit(exit_val)
302
303def add_test(suite, mod, name):
304 logging.info("Adding test " + mod.__name__ + "." + name)
305 suite.addTest(eval("mod." + name)())
306
Dan Talayco48370102010-03-03 15:17:33 -0800307#
308# Main script
309#
310
311# Get configuration, set up logging, import platform from file
312(config, args) = config_setup(config_default)
Dan Talayco48370102010-03-03 15:17:33 -0800313
Dan Talayco2c0dba32010-03-06 22:47:06 -0800314test_list_generate(config)
315
316# Check if test list is requested; display and exit if so
317if config["list"]:
318 print "\nTest List:"
319 for mod in config["all_tests"].keys():
320 print " Module: " + mod.__name__
321 for test in config["all_tests"][mod]:
322 print " " + test
323 sys.exit(0)
324
325logging_setup(config)
326logging.info("++++++++ " + time.asctime() + " ++++++++")
327
328# Generate the test suite
329#@todo Decide if multiple suites are ever needed
330suite = unittest.TestSuite()
331
332if config["test_spec"] == "all":
333 for mod in config["all_tests"].keys():
334 for test in config["all_tests"][mod]:
335 add_test(suite, mod, test)
336
337else:
338 for ts_entry in config["test_spec"].split(","):
339 parts = ts_entry.split(".")
340
341 if len(parts) == 1: # Either a module or test name
342 if ts_entry in config["mod_name_map"].keys():
343 mod = config["mod_name_map"][ts_entry]
344 for test in config["all_tests"][mod]:
345 add_test(suite, mod, test)
346 else: # Search for matching tests
347 test_found = False
348 for mod in config["all_tests"].keys():
349 if ts_entry in config["all_tests"][mod]:
350 add_test(suite, mod, ts_entry)
351 test_found = True
352 if not test_found:
353 die("Could not find module or test: " + ts_entry)
354
355 elif len(parts) == 2: # module.test
356 if parts[0] not in config["mod_name_map"]:
357 die("Unknown module in test spec: " + ts_entry)
358 mod = config["mod_name_map"][parts[0]]
359 if parts[1] in config["all_tests"][mod]:
360 add_test(suite, mod, parts[1])
361 else:
362 die("No known test matches: " + ts_entry)
363
364 else:
365 die("Bad test spec: " + ts_entry)
366
367# Check if platform specified
Dan Talayco48370102010-03-03 15:17:33 -0800368if config["platform"]:
369 _imp_string = "from " + config["platform"] + " import *"
Dan Talayco2c0dba32010-03-06 22:47:06 -0800370 logging.info("Importing platform: " + _imp_string)
Dan Talayco48370102010-03-03 15:17:33 -0800371 try:
372 exec(_imp_string)
373 except:
374 logging.warn("Failed to import " + config["platform"] + " file")
375
376try:
377 platform_config_update(config)
378except:
379 logging.warn("Could not run platform host configuration")
380
381if not config["port_map"]:
382 # Try to set up default port mapping if not done by platform
383 config["port_map"] = default_port_map_setup(config)
384
385if not config["port_map"]:
Dan Talayco2c0dba32010-03-06 22:47:06 -0800386 die("Interface port map is not defined. Exiting")
Dan Talayco48370102010-03-03 15:17:33 -0800387
388logging.debug("Configuration: " + str(config))
389logging.info("OF port map: " + str(config["port_map"]))
390
391# Init the test sets
Dan Talayco2c0dba32010-03-06 22:47:06 -0800392for (modname,mod) in config["mod_name_map"].items():
393 try:
394 mod.test_set_init(config)
395 except:
396 logging.warning("Could not run test_set_init for " + modname)
Dan Talayco48370102010-03-03 15:17:33 -0800397
Dan Talayco2c0dba32010-03-06 22:47:06 -0800398if config["dbg_level"] == logging.CRITICAL:
399 _verb = 0
400elif config["dbg_level"] >= logging.WARNING:
401 _verb = 1
402else:
403 _verb = 2
Dan Talayco48370102010-03-03 15:17:33 -0800404
Dan Talayco2c0dba32010-03-06 22:47:06 -0800405logging.info("*** TEST RUN START: " + time.asctime())
406unittest.TextTestRunner(verbosity=_verb).run(suite)
407logging.info("*** TEST RUN END : " + time.asctime())
Dan Talayco48370102010-03-03 15:17:33 -0800408