Index: server/site_wifitest.py |
diff --git a/server/site_wifitest.py b/server/site_wifitest.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..7fa2402a6c997c1eb48cdd72d5b17efdbb9099c9 |
--- /dev/null |
+++ b/server/site_wifitest.py |
@@ -0,0 +1,454 @@ |
+# Copyright (c) 2010 The Chromium OS Authors. All rights reserved. |
+# Use of this source code is governed by a BSD-style license that can be |
+# found in the LICENSE file. |
+ |
+import common, fnmatch, logging, os, re, string, threading, time |
+ |
+from autotest_lib.server import autotest, subcommand |
+from autotest_lib.server import site_bsd_router |
+#from autotest_lib.server import site_linux_router |
+ |
+class HelperThread(threading.Thread): |
+ # Class that wraps a ping command in a thread so it can run in the bg. |
+ def __init__(self, client, cmd): |
+ threading.Thread.__init__(self) |
+ self.client = client |
+ self.cmd = cmd |
+ |
+ def run(self): |
+ # NB: set ignore_status as we're always terminated w/ pkill |
+ self.client.run(self.cmd, ignore_status=True) |
+ |
+ |
+class WiFiTest(object): |
+ """ |
+ WiFi Test. |
+ |
+ Each test is specified as a dict. There must be a "name" entry that |
+ gives the test name (a string) and a "steps" entry that has an ordered |
+ tuple of test steps, where each step is a tuple [func, {args}]. |
+ |
+ Step funcs are one of: |
+ config configure the router/AP using the specified params |
+ (ssid automatically supplied); params are give as |
+ BSD ifconfig(8) parameters and translated to match |
+ the target router/AP's cli syntax |
+ deconfig de-configure/shut-off the router/AP |
+ connect connect client to AP using specified parameters |
+ (ssid automatically supplied) |
+ disconnect disconnect client from AP |
+ client_check_config check the client network connection to verify |
+ state is setup as expected |
+ sleep pause on the autotest server for a time |
+ client_ping ping the server on the client machine |
+ server_ping ping the client on the server machine |
+ client_iperf run iperf on the client to the server |
+ server_iperf run iperf on the server to the client |
+ client_netperf run netperf on the client to the server |
+ server_netperf run netperf on the server to the client |
+ |
+ Steps that are done on the client or server machine are implemented in |
+ this class. Steps that are done on the wifi router are implemented in |
+ a separate class that knows how to control the router. There are presently |
+ two classes: BSDRouter for routers based on FreeBSD and LinuxRouter for |
+ those based on Linux/mac80211. Additional router support can be added |
+ by adding a new class and auto-selecting it in __init__. |
+ |
+ The WiFiTest class could be generalized to handle clients other than |
+ ChromeOS; this would useful for systems that use Network Manager or |
+ wpa_supplicant directly. |
+ """ |
+ |
+ def __init__(self, name, steps, router, client, server): |
+ self.name = name |
+ self.steps = steps |
+ self.router = router['host'] |
+ self.client = client['host'] |
+ self.client_at = autotest.Autotest(self.client) |
+ self.client_wifi_ip = None # client's IP address on wifi net |
+ self.server = server['host'] |
+ self.server_at = autotest.Autotest(self.server) |
+ # server's IP address on wifi net (XXX assume same for now) |
+ self.server_wifi_ip = self.server.ip |
+ |
+ # NB: truncate SSID to 32 characters |
+ self.defssid = self.__get_defssid()[0:32] |
+ |
+ # XXX auto-detect router type |
+ self.wifi = site_bsd_router.BSDRouter(self.router, router, self.defssid) |
+ |
+ # potential bg thread for ping untilstop |
+ self.ping_thread = None |
+ |
+ |
+ def setup(self): |
+ self.job.setup_dep(['netperf']) |
+# XXX enable once built by build_autotest |
+# self.job.setup_dep(['iperf']) |
+ # create a empty srcdir to prevent the error that checks .version |
+ if not os.path.exists(self.srcdir): |
+ os.mkdir(self.srcdir) |
+ |
+ |
+ def cleanup(self, params): |
+ """ Cleanup state: disconnect client and destroy ap """ |
+ self.disconnect({}) |
+ self.wifi.destroy({}) |
+ |
+ |
+ def __get_defssid(self): |
+ # |
+ # Calculate ssid based on test name; this lets us track progress |
+ # by watching beacon frames. |
+ # |
+ # XXX truncate to 32 chars |
+ return re.sub('[^a-zA-Z0-9_]', '_', \ |
+ "%s_%s" % (self.name, self.router.ip)) |
+ |
+ |
+ def run(self): |
+ """ |
+ Run a WiFi test. Each step is interpreted as a method either |
+ in this class or the ancillary router class and invoked with |
+ the supplied parameter dictionary. |
+ """ |
+ for s in self.steps: |
+ method = s[0] |
+ if len(s) > 1: |
+ params = s[1] |
+ else: |
+ params = {} |
+ |
+ logging.info("%s: step '%s' params %s" % \ |
+ (self.name, method, params)) |
+ |
+ func = getattr(self, method, None) |
+ if func is None: |
+ func = getattr(self.wifi, method, None) |
+ if func is not None: |
+ try: |
+ func(params) |
+ except Exception, e: |
+ logging.error("%s: Step '%s' failed: %s; abort test" % \ |
+ (self.name, method, str(e))) |
+ self.cleanup(params) |
+ break |
+ else: |
+ logging.error("%s: Step '%s' unknown; abort test" % \ |
+ (self.name, method)) |
+ self.cleanup(params) |
+ break |
+ |
+ |
+ def __get_connect_script(self, params): |
+ return '\ |
+import dbus, dbus.mainloop.glib, gobject, logging, re, sys, time\n\ |
+\ |
+ssid = "' + params['ssid'] + '"\n\ |
+security = "' + params['security'] + '"\n\ |
+psk = "' + params.get('psk', "") + '"\n\ |
+assoc_timeout = ' + params.get('assoc_timeout', "15") + '\n\ |
+config_timeout = ' + params.get('config_timeout', "15") + '\n\ |
+\ |
+bus_loop = dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)\n\ |
+bus = dbus.SystemBus(mainloop=bus_loop)\n\ |
+manager = dbus.Interface(bus.get_object("org.moblin.connman", "/"),\n\ |
+ "org.moblin.connman.Manager")\n\ |
+\ |
+try:\n\ |
+ path = manager.GetService(({\n\ |
+ "Type": "wifi",\n\ |
+ "Mode": "managed",\n\ |
+ "SSID": ssid,\n\ |
+ "Security": security,\n\ |
+ "Passphrase": psk }))\n\ |
+ service = dbus.Interface(\n\ |
+ bus.get_object("org.moblin.connman", path),\n\ |
+ "org.moblin.connman.Service")\n\ |
+except Exception, e:\n\ |
+ print "FAIL(GetService): ssid %s exception %s" %(ssid, e)\n\ |
+ sys.exit(1)\n\ |
+\ |
+try:\n\ |
+ service.Connect()\n\ |
+except Exception, e:\n\ |
+ print "FAIL(Connect): ssid %s exception %s" %(ssid, e)\n\ |
+ sys.exit(2)\n\ |
+\ |
+status = ""\n\ |
+assoc_time = 0\n\ |
+# wait up to assoc_timeout seconds to associate\n\ |
+while assoc_time < assoc_timeout:\n\ |
+ properties = service.GetProperties()\n\ |
+ status = properties.get("State", None)\n\ |
+# print>>sys.stderr, "time %3.1f state %s" % (assoc_time, status)\n\ |
+ if status == "failure":\n\ |
+ print "FAIL(assoc): ssid %s assoc %3.1f secs props %s" %(ssid, assoc_time, properties)\n\ |
+ sys.exit(3)\n\ |
+ if status == "configuration" or status == "ready":\n\ |
+ break\n\ |
+ time.sleep(.5)\n\ |
+ assoc_time += .5\n\ |
+if assoc_time >= assoc_timeout:\n\ |
+ print "TIMEOUT(assoc): ssid %s assoc %3.1f secs" %(ssid, assoc_time)\n\ |
+ sys.exit(4)\n\ |
+\ |
+# wait another config_timeout seconds to get an ip address\n\ |
+config_time = 0\n\ |
+if status != "ready":\n\ |
+ while config_time < config_timeout:\n\ |
+ properties = service.GetProperties()\n\ |
+ status = properties.get("State", None)\n\ |
+# print>>sys.stderr, "time %3.1f state %s" % (config_time, status)\n\ |
+ if status == "failure":\n\ |
+ print "FAIL(config): ssid %s assoc %3.1f config %3.1f secs" \\\n\ |
+ %(ssid, assoc_time, config_time)\n\ |
+ sys.exit(5)\n\ |
+ if status == "ready":\n\ |
+ break\n\ |
+ time.sleep(.5)\n\ |
+ config_time += .5\n\ |
+ if config_time >= config_timeout:\n\ |
+ print "TIMEOUT(config): ssid %s assoc %3.1f config %3.1f secs"\\\n\ |
+ %(ssid, assoc_time, config_time)\n\ |
+ sys.exit(6)\n\ |
+print "assoc %3.1f secs config %3.1f secs" % (assoc_time, config_time)\n\ |
+sys.exit(0)' |
+ |
+ |
+ def __get_ipaddr(self, host, ifnet): |
+ # XXX gotta be a better way to do this |
+ result = host.run("ifconfig %s" % ifnet) |
+ m = re.search('inet addr:([^ ]*)', result.stdout) |
+ if m is None: |
+ raise Except, "No inet address found" |
+ return m.group(1) |
+ |
+ |
+ def connect(self, params): |
+ """ Connect client to AP/router """ |
+ if 'ssid' not in params: |
+ params['ssid'] = self.defssid |
+ script = self.__get_connect_script(params) |
+ result = self.client.run("python<<'EOF'\n%s\nEOF\n" % script) |
+ print "%s: %s" % (self.name, result.stdout[0:-1]) |
+ |
+ # fetch IP address of wireless device |
+ # XXX wlan0 hard coded |
+ self.client_wifi_ip = self.__get_ipaddr(self.client, "wlan0") |
+ logging.info("%s: client WiFi-IP is %s" % \ |
+ (self.name, self.client_wifi_ip)) |
+ |
+ |
+ def __make_disconnect_script(self, params): |
+ return '\ |
+import dbus, dbus.mainloop.glib, gobject, logging, sys, time\n\ |
+\n\ |
+interface = "' + params.get('psk', "") + '"\n\ |
+\n\ |
+bus_loop = dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)\n\ |
+bus = dbus.SystemBus(mainloop=bus_loop)\n\ |
+manager = dbus.Interface(bus.get_object("org.moblin.connman", "/"),\n\ |
+ "org.moblin.connman.Manager")\n\ |
+\n\ |
+sys.exit(0)' |
+ |
+ |
+ def disconnect(self, params): |
+ """ Disconnect previously connected client """ |
+ self.client_ping_bg_stop({}) |
+# script = self.__make_disconnect_script(params) |
+# self.client.run("python<<'EOF'\n%s\nEOF\n" % script) |
+ |
+ |
+ def sleep(self, params): |
+ time.sleep(float(params['time'])) |
+ |
+ |
+ def __ping_args(self, params): |
+ args = "" |
+ if 'count' in params: |
+ args += " -c %s" % params['count'] |
+ if 'size' in params: |
+ args += " -s %s" % params['size'] |
+ if 'bcast' in params: |
+ args += " -b" |
+ if 'flood' in params: |
+ args += " -f" |
+ if 'interval' in params: |
+ args += " -i %s" % params['interval'] |
+ if 'qos' in params: |
+ ac = string.lower(params['qos']) |
+ if ac == 'be': |
+ args += " -Q 0x04" |
+ elif ac == 'bk': |
+ args += " -Q 0x02" |
+ elif ac == 'vi': |
+ args += " -Q 0x08" |
+ elif ac == 'vo': |
+ args += " -Q 0x10" |
+ else: |
+ args += " -Q %s" % ac |
+ return args |
+ |
+ |
+ def __get_pingstats(self, str): |
+ stats = {} |
+ m = re.search('([0-9]*) packets transmitted,[ ]*([0-9]*)[ ]' |
+ 'received, ([0-9]*)', str) |
+ if m is not None: |
+ stats['xmit'] = m.group(1) |
+ stats['recv'] = m.group(2) |
+ stats['loss'] = m.group(3) |
+ m = re.search('rtt min[^=]*= ([0-9.]*)/([0-9.]*)/([0-9.]*)', str) |
+ if m is not None: |
+ stats['min'] = m.group(1) |
+ stats['avg'] = m.group(2) |
+ stats['max'] = m.group(3) |
+ return stats |
+ |
+ |
+ def __print_pingstats(self, label, stats): |
+ logging.info("%s: %s%s/%s, %s%% loss, rtt %s/%s/%s" % \ |
+ (self.name, label, stats['xmit'], stats['recv'], stats['loss'], |
+ stats['min'], stats['avg'], stats['max'])) |
+ |
+ |
+ def client_ping(self, params): |
+ """ Ping the server from the client """ |
+ ping_ip = params.get('ping_ip', self.server_wifi_ip) |
+ count = params.get('count', 10) |
+ # set timeout for 3s / ping packet |
+ result = self.client.run("ping %s %s" % \ |
+ (self.__ping_args(params), ping_ip), timeout=3*int(count)) |
+ |
+ self.__print_pingstats("client_ping ", |
+ self.__get_pingstats(result.stdout)) |
+ |
+ |
+ def client_ping_bg(self, params): |
+ """ Ping the server from the client """ |
+ ping_ip = params.get('ping_ip', self.server_wifi_ip) |
+ cmd = "ping %s %s" % (self.__ping_args(params), ping_ip) |
+ self.ping_thread = HelperThread(self.client, cmd) |
+ self.ping_thread.start() |
+ |
+ |
+ def client_ping_bg_stop(self, params): |
+ if self.ping_thread is not None: |
+ self.client.run("pkill ping") |
+ self.ping_thread.join() |
+ self.ping_thread = None |
+ |
+ |
+ def server_ping(self, params): |
+ """ Ping the client from the server """ |
+ ping_ip = params.get('ping_ip', self.client_wifi_ip) |
+ # XXX 30 second timeout |
+ result = self.server.run("ping %s %s" % \ |
+ (self.__ping_args(params), ping_ip), timeout=30) |
+ |
+ self.__print_pingstats("server_ping ", |
+ self.__get_pingstats(result.stdout)) |
+ |
+ |
+ |
+ def __run_iperf(self, client_ip, server_ip, params): |
+ template = ''.join(["job.run_test('iperf', \ |
+ server_ip='%s', client_ip='%s', role='%s'%s"]) |
+ if 'udp' in params: |
+ template += ", udp=True" |
+ if 'bidir' in params: |
+ template += ", bidirectional=True" |
+ if 'time' in params: |
+ template += ", test_time=%s" % params['time'] |
+ template += ")" |
+ |
+ server_control_file = template % (server_ip, client_ip, 'server') |
+ server_command = subcommand.subcommand(self.server_at.run, |
+ [server_control_file, self.server.hostname]) |
+ |
+ client_control_file = template % (server_ip, client_ip, 'client') |
+ client_command = subcommand.subcommand(self.client_at.run, |
+ [client_control_file, self.client.hostname]) |
+ |
+ logging.info("%s: iperf %s => %s" % (self.name, client_ip, server_ip)) |
+ |
+ # XXX 30 sec timeout for now |
+ subcommand.parallel([server_command, client_command], timeout=30) |
+ |
+ |
+ def client_iperf(self, params): |
+ """ Run iperf on the client against the server """ |
+ self.__run_iperf(self.client_wifi_ip, self.server_wifi_ip, params) |
+ |
+ |
+ def server_iperf(self, params): |
+ """ Run iperf on the server against the client """ |
+ self.__run_iperf(self.server_wifi_ip, self.client_wifi_ip, params) |
+ |
+ |
+ def __run_netperf(self, client_ip, server_ip, params): |
+ template = ''.join(["job.run_test('netperf2', \ |
+ server_ip='%s', client_ip='%s', role='%s'"]) |
+ if 'test' in params: |
+ template += ", test='%s'" % params['test'] |
+ if 'bidir' in params: |
+ template += ", bidi=True" |
+ if 'time' in params: |
+ template += ", test_time=%s" % params['time'] |
+ template += ")" |
+ |
+ server_control_file = template % (server_ip, client_ip, 'server') |
+ server_command = subcommand.subcommand(self.server_at.run, |
+ [server_control_file, self.server.hostname]) |
+ |
+ client_control_file = template % (server_ip, client_ip, 'client') |
+ client_command = subcommand.subcommand(self.client_at.run, |
+ [client_control_file, self.client.hostname]) |
+ |
+ logging.info("%s: netperf %s => %s" % (self.name, client_ip, server_ip)) |
+ |
+ # XXX 30 sec timeout for now |
+ subcommand.parallel([server_command, client_command], timeout=60) |
+ |
+ |
+ def client_netperf(self, params): |
+ """ Run netperf on the client against the server """ |
+ self.__run_netperf(self.client_wifi_ip, self.server_wifi_ip, params) |
+ |
+ |
+ def server_netperf(self, params): |
+ """ Run netperf on the server against the client """ |
+ self.__run_netperf(self.server_wifi_ip, self.client_wifi_ip, params) |
+ |
+ |
+def __byfile(a, b): |
+ if a['file'] < b['file']: |
+ return -1 |
+ elif a['file'] > b['file']: |
+ return 1 |
+ else: |
+ return 0 |
+ |
+def read_tests(dir, pat): |
+ """ |
+ Collect WiFi test tuples from files. File names are used to |
+ sort the test objects so the convention is to name them NNN<test> |
+ where NNN is a decimal number used to sort and <test> is an |
+ identifying name for the test; e.g. 000Check11b |
+ """ |
+ tests = [] |
+ for file in os.listdir(dir): |
+ if fnmatch.fnmatch(file, pat): |
+ fd = open(os.path.join(dir, file)); |
+ try: |
+ test = eval(fd.read()) |
+ except Exception, e: |
+ logging.error("%s: %s" % (os.path.join(dir, file), str(e))) |
+ raise e |
+ test['file'] = file |
+ tests.append(test) |
+ # use filenames to sort |
+ return sorted(tests, cmp=__byfile) |
+ |