blob: cae63b6af7bf6eaf6ea8380301cd5b41a268fdc7 [file] [log] [blame]
Sreeju Sreedhare3fefd92019-04-02 15:57:15 -07001#!/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 using the OFT
8framework. For usage information, see --help or the README.
9
10To add a new command line option, edit both the CONFIG_DEFAULT dictionary and
11the config_setup function. The option's result will end up in the global
12oftest.config dictionary.
13"""
14
15from __future__ import print_function
16
17import sys
18import optparse
19import logging
20import unittest
21import time
22import os
23import imp
24import random
25import signal
26import fnmatch
27import copy
28
29ROOT_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), os.pardir)
30LOG_DIR = os.path.join(ROOT_DIR, "Log")+time.strftime("/%Y-%m/%d%H%M%S")
31#LOG_FILE = time.strftime("%H%M%S")+"oft.log"
32PY_SRC_DIR = os.path.join(ROOT_DIR, 'Utilities', 'src', 'python')
33if os.path.exists(os.path.join(PY_SRC_DIR, 'oftest')):
34 # Running from source tree
35 sys.path.insert(0, PY_SRC_DIR)
36
37if os.path.exists(os.path.join(ROOT_DIR, 'Utilities', 'accton')):
38 PY_ACCTON_DIR = os.path.join(ROOT_DIR, 'Utilities', 'accton')
39 sys.path.insert(0, PY_ACCTON_DIR)
40
41import oftest
42from oftest import config
43import oftest.ofutils
44import oftest.help_formatter
45import loxi
46
47##@var DEBUG_LEVELS
48# Map from strings to debugging levels
49DEBUG_LEVELS = {
50 'debug' : logging.DEBUG,
51 'verbose' : logging.DEBUG,
52 'info' : logging.INFO,
53 'warning' : logging.WARNING,
54 'warn' : logging.WARNING,
55 'error' : logging.ERROR,
56 'critical' : logging.CRITICAL
57}
58
59##@var CONFIG_DEFAULT
60# The default configuration dictionary for OFT
61CONFIG_DEFAULT = {
62 # Miscellaneous options
63 "list" : False,
64 "list_test_names" : False,
65 "allow_user" : False,
66
67 # Test selection options
68 "test_spec" : "",
69 "test_file" : None,
70 "test_dir" : os.path.join(ROOT_DIR, "Tests"),
71
72 # Switch connection options
73 "controller_host" : "0.0.0.0", # For passive bind
74 "controller_port" : 6653,
75 "switch_ip" : None, # If not none, actively connect to switch
76 "switch_type" : None, # If not none, adapt flows to pipeline differences for switch type
77 "platform" : "eth",
78 "platform_args" : None,
79 "platform_dir" : os.path.join(ROOT_DIR, "Utilities", "platforms"),
80 "interfaces" : [],
81 "openflow_version" : "1.0",
82
83 # Logging options
84 "log_file" : time.strftime("%H%M%S")+"oft.log",
85 "log_dir" : LOG_DIR,
86 "debug" : "verbose",
87 "profile" : False,
88 "profile_file" : "profile.out",
89 "xunit" : False,
90 "xunit_dir" : "xunit",
91
92 # Test behavior options
93 "relax" : False,
94 "test_params" : "None",
95 "fail_skipped" : False,
96 "default_timeout" : 2.0,
97 "default_negative_timeout" : 0.01,
98 "minsize" : 0,
99 "random_seed" : None,
100 "disable_ipv6" : False,
101 "random_order" : False,
102 "dump_packet" : True,
103 "cicada_poject" : False,
104 "force_ofdpa_restart": False,
105 # Other configuration
106 "port_map" : {},
107}
108
109def config_setup():
110 """
111 Set up the configuration including parsing the arguments
112
113 @return A pair (config, args) where config is an config
114 object and args is any additional arguments from the command line
115 """
116
117 usage = "usage: %prog [options] (test|group)..."
118
119 description = """\
120OFTest is a framework and set of tests for validating OpenFlow switches.
121
122The default configuration assumes that an OpenFlow 1.0 switch is attempting to
123connect to a controller on the machine running OFTest, port 6653. Additionally,
124the interfaces veth1, veth3, veth5, and veth7 should be connected to the switch's
125dataplane.
126
127If no positional arguments are given then OFTest will run all tests that
128depend only on standard OpenFlow 1.0. Otherwise each positional argument
129is interpreted as either a test name or a test group name. The union of
130these will be executed. To see what groups each test belongs to use the
131--list option. Tests and groups can be subtracted from the result by
132prefixing them with the '^' character.
133"""
134
135 # Parse --interface
136 def check_interface(option, opt, value):
137 try:
138 ofport, interface = value.split('@', 1)
139 ofport = int(ofport)
140 except ValueError:
141 raise optparse.OptionValueError("incorrect interface syntax (got %s, expected 'ofport@interface')" % repr(value))
142 return (ofport, interface)
143
144 class Option(optparse.Option):
145 TYPES = optparse.Option.TYPES + ("interface",)
146 TYPE_CHECKER = copy.copy(optparse.Option.TYPE_CHECKER)
147 TYPE_CHECKER["interface"] = check_interface
148
149 parser = optparse.OptionParser(version="%prog 0.1",
150 usage=usage,
151 description=description,
152 formatter=oftest.help_formatter.HelpFormatter(),
153 option_class=Option)
154
155 # Set up default values
156 parser.set_defaults(**CONFIG_DEFAULT)
157
158 parser.add_option("--list", action="store_true",
159 help="List all tests and exit")
160 parser.add_option("--list-test-names", action='store_true',
161 help="List test names matching the test spec and exit")
162 parser.add_option("--allow-user", action="store_true",
163 help="Proceed even if oftest is not run as root")
164
165 group = optparse.OptionGroup(parser, "Test selection options")
166 group.add_option("-T", "--test-spec", "--test-list", help="Tests to run, separated by commas")
167 group.add_option("-f", "--test-file", help="File of tests to run, one per line")
168 group.add_option("--test-dir", type="string", help="Directory containing tests")
169 parser.add_option_group(group)
170
171 group = optparse.OptionGroup(parser, "Switch connection options")
172 group.add_option("-H", "--host", dest="controller_host",
173 help="IP address to listen on (default %default)")
174 group.add_option("-p", "--port", dest="controller_port",
175 type="int", help="Port number to listen on (default %default)")
176 group.add_option("-S", "--switch-ip", dest="switch_ip",
177 help="If set, actively connect to this switch by IP")
178 group.add_option("-Y", "--switch-type", dest="switch_type",
179 help="If set to qmx, flows will adapt to pipline differences")
180 group.add_option("-P", "--platform", help="Platform module name (default %default)")
181 group.add_option("-a", "--platform-args", help="Custom arguments for the platform")
182 group.add_option("--platform-dir", type="string", help="Directory containing platform modules")
183 group.add_option("--interface", "-i", type="interface", dest="interfaces", metavar="INTERFACE", action="append",
184 help="Specify a OpenFlow port number and the dataplane interface to use. May be given multiple times. Example: 1@eth1")
185 group.add_option("--of-version", "-V", dest="openflow_version", choices=loxi.version_names.values(),
186 help="OpenFlow version to use")
187 parser.add_option_group(group)
188
189 group = optparse.OptionGroup(parser, "Logging options")
190 group.add_option("--log-file", help="Name of log file (default %default)")
191 group.add_option("--log-dir", help="Name of log directory")
192 dbg_lvl_names = sorted(DEBUG_LEVELS.keys(), key=lambda x: DEBUG_LEVELS[x])
193 group.add_option("--debug", choices=dbg_lvl_names,
194 help="Debug lvl: debug, info, warning, error, critical (default %default)")
195 group.add_option("-v", "--verbose", action="store_const", dest="debug",
196 const="verbose", help="Shortcut for --debug=verbose")
197 group.add_option("-q", "--quiet", action="store_const", dest="debug",
198 const="warning", help="Shortcut for --debug=warning")
199 group.add_option("--profile", action="store_true", help="Enable Python profiling")
200 group.add_option("--profile-file", help="Output file for Python profiler")
201 group.add_option("--xunit", action="store_true", help="Enable xUnit-formatted results")
202 group.add_option("--xunit-dir", help="Output directory for xUnit-formatted results")
203 parser.add_option_group(group)
204
205 group = optparse.OptionGroup(parser, "Test behavior options")
206 group.add_option("--relax", action="store_true",
207 help="Relax packet match checks allowing other packets")
208 test_params_help = """Set test parameters: key=val;... (see --list)
209 """
210 group.add_option("-t", "--test-params", help=test_params_help)
211 group.add_option("--fail-skipped", action="store_true",
212 help="Return failure if any test was skipped")
213 group.add_option("--default-timeout", type=float,
214 help="Timeout in seconds for most operations")
215 group.add_option("--default-negative-timeout", type=float,
216 help="Timeout in seconds for negative checks")
217 group.add_option("--minsize", type="int",
218 help="Minimum allowable packet size on the dataplane.")
219 group.add_option("--random-seed", type="int",
220 help="Random number generator seed")
221 group.add_option("--force-ofdpa-restart",
222 help="If set force ofdpa restart on user@switchIP")
223 group.add_option("--disable-ipv6", action="store_true",
224 help="Disable IPv6 tests")
225 group.add_option("--random-order", action="store_true",
226 help="Randomize order of tests")
227 group.add_option("--dump_packet", action="store_true",
228 help="Dump packet content on log when verify packet fail")
229 group.add_option("--cicada_poject", action="store_true",
230 help="True verify Cicada behavior, False verify AOS behaviro")
231 parser.add_option_group(group)
232
233 # Might need this if other parsers want command line
234 # parser.allow_interspersed_args = False
235 (options, args) = parser.parse_args()
236
237 # If --test-dir wasn't given, pick one based on the OpenFlow version
238# if options.test_dir == None:
239# if options.openflow_version == "1.3":
240# options.test_dir = os.path.join(ROOT_DIR, "Test", "ofdpa")
241
242 # Convert options from a Namespace to a plain dictionary
243 config = CONFIG_DEFAULT.copy()
244 for key in config.keys():
245 config[key] = getattr(options, key)
246
247 return (config, args)
248
249#def logging_directory()
250 """
251 creates a specific log directory based on day of execution
252 """
253# log_dir=os.path.join(ROOT_DIR, "Log")+time.strftime("/%Y/%m/%d")
254# return(log_dir)
255
256def logging_setup(config):
257 """
258 Set up logging based on config
259 """
260
261 logging.getLogger().setLevel(DEBUG_LEVELS[config["debug"]])
262
263 if config["log_dir"] != None:
264 if not os.path.exists(config["log_dir"]):
265 os.makedirs(config["log_dir"])
266 else:
267 if os.path.exists(config["log_file"]):
268 os.remove(config["log_file"])
269
270 oftest.open_logfile('main')
271
272def xunit_setup(config):
273 """
274 Set up xUnit output based on config
275 """
276
277 if not config["xunit"]:
278 return
279
280 if os.path.exists(config["xunit_dir"]):
281 import shutil
282 shutil.rmtree(config["xunit_dir"])
283 os.makedirs(config["xunit_dir"])
284
285def pcap_setup(config):
286 """
287 Set up dataplane packet capturing based on config
288 """
289
290 if config["log_dir"] == None:
291 filename = os.path.splitext(config["log_file"])[0] + '.pcap'
292 oftest.dataplane_instance.start_pcap(filename)
293 else:
294 # start_pcap is called per-test in base_tests
295 pass
296
297def profiler_setup(config):
298 """
299 Set up profiler based on config
300 """
301
302 if not config["profile"]:
303 return
304
305 import cProfile
306 profiler = cProfile.Profile()
307 profiler.enable()
308
309 return profiler
310
311def profiler_teardown(profiler):
312 """
313 Tear down profiler based on config
314 """
315
316 if not config["profile"]:
317 return
318
319 profiler.disable()
320 profiler.dump_stats(config["profile_file"])
321
322
323def load_test_modules(config):
324 """
325 Load tests from the test_dir directory.
326
327 Test cases are subclasses of unittest.TestCase
328
329 Also updates the _groups member to include "standard" and
330 module test groups if appropriate.
331
332 @param config The oft configuration dictionary
333 @returns A dictionary from test module names to tuples of
334 (module, dictionary from test names to test classes).
335 """
336
337 result = {}
338
339 for root, dirs, filenames in os.walk(config["test_dir"]):
340 # Iterate over each python file
341 for filename in fnmatch.filter(filenames, '[!.]*.py'):
342 modname = os.path.splitext(os.path.basename(filename))[0]
343
344 try:
345 if sys.modules.has_key(modname):
346 mod = sys.modules[modname]
347 else:
348 mod = imp.load_module(modname, *imp.find_module(modname, [root]))
349 except:
350 logging.warning("Could not import file " + filename)
351 raise
352
353 # Find all testcases defined in the module
354 tests = dict((k, v) for (k, v) in mod.__dict__.items() if type(v) == type and
355 issubclass(v, unittest.TestCase) and
356 hasattr(v, "runTest"))
357 if tests:
358 for (testname, test) in tests.items():
359 # Set default annotation values
360 if not hasattr(test, "_groups"):
361 test._groups = []
362 if not hasattr(test, "_nonstandard"):
363 test._nonstandard = False
364 if not hasattr(test, "_disabled"):
365 test._disabled = False
366
367 # Put test in its module's test group
368 if not test._disabled:
369 test._groups.append(modname)
370
371 # Put test in the standard test group
372 if not test._disabled and not test._nonstandard:
373 test._groups.append("standard")
374 test._groups.append("all") # backwards compatibility
375
376 result[modname] = (mod, tests)
377
378 return result
379
380def prune_tests(test_specs, test_modules, version):
381 """
382 Return tests matching the given test-specs and OpenFlow version
383 @param test_specs A list of group names or test names.
384 @param version An OpenFlow version (e.g. "1.0")
385 @param test_modules Same format as the output of load_test_modules.
386 @returns Same format as the output of load_test_modules.
387 """
388 result = {}
389 for e in test_specs:
390 matched = False
391
392 if e.startswith('^'):
393 negated = True
394 e = e[1:]
395 else:
396 negated = False
397
398 for (modname, (mod, tests)) in test_modules.items():
399 for (testname, test) in tests.items():
400 if e in test._groups or e == "%s.%s" % (modname, testname):
401 result.setdefault(modname, (mod, {}))
402 if not negated:
403 if not hasattr(test, "_versions") or version in test._versions:
404 result[modname][1][testname] = test
405 else:
406 if modname in result and testname in result[modname][1]:
407 del result[modname][1][testname]
408 if not result[modname][1]:
409 del result[modname]
410 matched = True
411
412 if not matched:
413 die("test-spec element %s did not match any tests" % e)
414
415 return result
416
417def die(msg, exit_val=1):
418 logging.critical(msg)
419 sys.exit(exit_val)
420
421#
422# Main script
423#
424
425# Setup global configuration
426(new_config, args) = config_setup()
427oftest.config.update(new_config)
428
429logging_setup(config)
430xunit_setup(config)
431logging.info("++++++++ " + time.asctime() + " ++++++++")
432
433# Pick an OpenFlow protocol module based on the configured version
434name_to_version = dict((v,k) for k, v in loxi.version_names.iteritems())
435sys.modules["ofp"] = loxi.protocol(name_to_version[config["openflow_version"]])
436
437# HACK: testutils.py imports controller.py, which needs the ofp module
438import oftest.testutils
439
440# Allow tests to import each other
441sys.path.append(config["test_dir"])
442
443test_specs = args
444if config["test_spec"] != "":
445 logging.warning("The '--test-spec' option is deprecated.")
446 test_specs += config["test_spec"].split(',')
447if config["test_file"] != None:
448 with open(config["test_file"], 'r') as f:
449 for line in f:
450 line, _, _ = line.partition('#') # remove comments
451 line = line.strip()
452 if line:
453 test_specs.append(line)
454if test_specs == []:
455 test_specs = ["standard"]
456
457test_modules = load_test_modules(config)
458
459# Check if test list is requested; display and exit if so
460if config["list"]:
461 mod_count = 0
462 test_count = 0
463 all_groups = set()
464
465 print("""
466Tests are shown grouped by module. If a test is in any groups beyond
467"standard" and its module's group then they are shown in parentheses.
468
469Tests marked with '*' are non-standard and may require vendor extensions or
470special switch configuration. These are not part of the "standard" test group.
471
472Tests marked with '!' are disabled because they are experimental,
473special-purpose, or are too long to be run normally. These are not part of
474the "standard" test group or their module's test group.
475
476Tests marked (TP1) after name take --test-params including:
477
478 'vid=N;strip_vlan=bool;add_vlan=bool'
479
480Test List:
481""")
482 for (modname, (mod, tests)) in test_modules.items():
483 mod_count += 1
484 desc = (mod.__doc__ or "No description").strip().split('\n')[0]
485 print(" Module %13s: %s" % (mod.__name__, desc))
486
487 for (testname, test) in tests.items():
488 desc = (test.__doc__ or "No description").strip().split('\n')[0]
489
490 groups = set(test._groups) - set(["all", "standard", modname])
491 all_groups.update(test._groups)
492 if groups:
493 desc = "(%s) %s" % (",".join(groups), desc)
494 if hasattr(test, "_versions"):
495 desc = "(%s) %s" % (",".join(sorted(test._versions)), desc)
496
497 start_str = " %s%s %s:" % (test._nonstandard and "*" or " ",
498 test._disabled and "!" or " ",
499 testname)
500 print(" %22s : %s" % (start_str, desc))
501 test_count += 1
502 print
503 print("'%d' modules shown with a total of '%d' tests\n" %
504 (mod_count, test_count))
505 print("Test groups: %s" % (', '.join(sorted(all_groups))))
506
507 sys.exit(0)
508
509test_modules = prune_tests(test_specs, test_modules, config["openflow_version"])
510
511# Check if test list is requested; display and exit if so
512if config["list_test_names"]:
513 for (modname, (mod, tests)) in test_modules.items():
514 for (testname, test) in tests.items():
515 print("%s.%s" % (modname, testname))
516
517 sys.exit(0)
518
519# Generate the test suite
520#@todo Decide if multiple suites are ever needed
521suite = unittest.TestSuite()
522
523sorted_tests = []
524for (modname, (mod, tests)) in sorted(test_modules.items()):
525 for (testname, test) in sorted(tests.items()):
526 sorted_tests.append(test)
527
528if config["random_order"]:
529 random.shuffle(sorted_tests)
530
531for test in sorted_tests:
532 suite.addTest(test())
533
534# Allow platforms to import each other
535sys.path.append(config["platform_dir"])
536
537# Load the platform module
538platform_name = config["platform"]
539logging.info("Importing platform: " + platform_name)
540platform_mod = None
541try:
542 platform_mod = imp.load_module(platform_name, *imp.find_module(platform_name, [config["platform_dir"]]))
543except:
544 logging.warn("Failed to import " + platform_name + " platform module")
545 raise
546
547try:
548 platform_mod.platform_config_update(config)
549except:
550 logging.warn("Could not run platform host configuration")
551 raise
552
553if not config["port_map"]:
554 die("Interface port map was not defined by the platform. Exiting.")
555
556logging.debug("Configuration: " + str(config))
557logging.info("OF port map: " + str(config["port_map"]))
558
559oftest.ofutils.default_timeout = config["default_timeout"]
560oftest.ofutils.default_negative_timeout = config["default_negative_timeout"]
561oftest.testutils.MINSIZE = config['minsize']
562
563if os.getuid() != 0 and not config["allow_user"]:
564 die("Super-user privileges required. Please re-run with sudo or as root.")
565 sys.exit(1)
566
567if config["random_seed"] is not None:
568 logging.info("Random seed: %d" % config["random_seed"])
569 random.seed(config["random_seed"])
570else:
571 # Generate random seed and report to log file
572 seed = random.randrange(100000000)
573 logging.info("Autogen random seed: %d" % seed)
574 random.seed(seed)
575
576# Remove python's signal handler which raises KeyboardError. Exiting from an
577# exception waits for all threads to terminate which might not happen.
578signal.signal(signal.SIGINT, signal.SIG_DFL)
579
580if __name__ == "__main__":
581 profiler = profiler_setup(config)
582
583 # Set up the dataplane
584 oftest.dataplane_instance = oftest.dataplane.DataPlane(config)
585 pcap_setup(config)
586 for of_port, ifname in config["port_map"].items():
587 oftest.dataplane_instance.port_add(ifname, of_port)
588
589 logging.info("*** TEST RUN START: " + time.asctime())
590 if config["xunit"]:
591 try:
592 import xmlrunner # fail-fast if module missing
593 except ImportError as ex:
594 oftest.dataplane_instance.kill()
595 profiler_teardown(profiler)
596 raise ex
597 runner = xmlrunner.XMLTestRunner(output=config["xunit_dir"],
598 outsuffix="",
599 verbosity=2)
600 result = runner.run(suite)
601 else:
602 result = unittest.TextTestRunner(verbosity=2).run(suite)
603 oftest.open_logfile('main')
604 if oftest.testutils.skipped_test_count > 0:
605 message = "Skipped %d test(s)" % oftest.testutils.skipped_test_count
606 logging.info(message)
607 logging.info("*** TEST RUN END : %s", time.asctime())
608
609 # Shutdown the dataplane
610 oftest.dataplane_instance.kill()
611 oftest.dataplane_instance = None
612
613 profiler_teardown(profiler)
614
615 if result.failures or result.errors:
616 # exit(1) hangs sometimes
617 os._exit(1)
618 if oftest.testutils.skipped_test_count > 0 and config["fail_skipped"]:
619 os._exit(1)