| # 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, hosts, subcommand |
| from autotest_lib.server import site_bsd_router |
| from autotest_lib.server import site_linux_router |
| |
| class NotImplemented(Exception): |
| def __init__(self, what): |
| self.what = what |
| |
| |
| def __str__(self): |
| return repr("Test method '%s' not implemented" % self.what) |
| |
| |
| 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_* check the client's connection state to verify |
| a parameter was setup as expected; e.g. |
| client_check_bintval checks the beacon interval |
| set on the AP was adopted by the client |
| client_monitor_start start monitoring for wireless system events as |
| needed (e.g, kick off a process that listens) |
| client_monitor_stop stop monitoring for wireless system events |
| client_check_event_* check the client's event log for an event; |
| should always be preceded by client_monitor_start |
| 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, config): |
| self.name = name |
| self.steps = steps |
| self.keyvals = {} |
| |
| router = config['router'] |
| self.router = hosts.create_host(router['addr']) |
| # NB: truncate SSID to 32 characters |
| self.defssid = self.__get_defssid(router['addr'])[0:32] |
| |
| if 'type' not in router: |
| # auto-detect router type |
| if site_linux_router.isLinuxRouter(self.router): |
| router['type'] = 'linux' |
| elif site_bsd_router.isBSDRouter(self.router): |
| router['type'] = 'bsd' |
| else: |
| raise error.TestFail('Unable to autodetect router type') |
| if router['type'] == 'linux': |
| self.wifi = site_linux_router.LinuxRouter(self.router, router, |
| self.defssid) |
| elif router['type'] == 'bsd': |
| self.wifi = site_bsd_router.BSDRouter(self.router, router, |
| self.defssid) |
| else: |
| raise error.TestFail('Unsupported router') |
| |
| # |
| # The client machine must be reachable from the control machine. |
| # The address on the wifi network is retrieved each time it |
| # associates to the router. |
| # |
| client = config['client'] |
| self.client = hosts.create_host(client['addr']) |
| self.client_at = autotest.Autotest(self.client) |
| self.client_wifi_ip = None # client's IP address on wifi net |
| # interface name on client |
| self.client_wlanif = client.get('wlandev', "wlan0") |
| |
| # |
| # The server machine may be multi-homed or only on the wifi |
| # network. When only on the wifi net we suppress server_* |
| # requests since we cannot initiate them from the control machine. |
| # |
| server = config['server'] |
| # NB: server may not be reachable on the control network |
| if 'addr' in server: |
| self.server = hosts.create_host(server['addr']) |
| self.server_at = autotest.Autotest(self.server) |
| # if not specified assume the same as the control address |
| self.server_wifi_ip = getattr(server, 'wifi_addr', self.server.ip) |
| else: |
| self.server = None; |
| # NB: wifi address must be set if not reachable from control |
| self.server_wifi_ip = server['wifi_addr'] |
| |
| # potential bg thread for ping untilstop |
| self.ping_thread = None |
| |
| |
| def cleanup(self, params): |
| """ Cleanup state: disconnect client and destroy ap """ |
| self.disconnect({}) |
| self.wifi.destroy({}) |
| |
| |
| def __get_defssid(self, ipaddr): |
| # |
| # Calculate ssid based on test name; this lets us track progress |
| # by watching beacon frames. |
| # |
| return re.sub('[^a-zA-Z0-9_]', '_', "%s_%s" % (self.name, ipaddr)) |
| |
| |
| 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) |
| raise e |
| break |
| else: |
| logging.error("%s: Step '%s' unknown; abort test", |
| self.name, method) |
| self.cleanup(params) |
| break |
| |
| |
| 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 error.TestFail("No inet address found") |
| return m.group(1) |
| |
| |
| def connect(self, params): |
| """ Connect client to AP/router """ |
| |
| # copy site_wlan_connect.py over |
| script_name = 'site_wlan_connect.py' |
| script_client_file = self.client.get_tmp_dir() + '/' + script_name |
| self.client.send_file( |
| os.path.dirname(os.path.realpath(__file__)) + '/' + script_name, |
| script_client_file, |
| delete_dest=True) |
| |
| result = self.client.run('python "%s" "%s" "%s" "%s" "%d" "%d"' % |
| (script_client_file, |
| params.get('ssid', self.defssid), |
| params.get('security', ''), |
| params.get('psk', ''), |
| params.get('assoc_timeout', 15), |
| params.get('config_timeout', 15))).stdout.rstrip() |
| |
| result_times = re.match("OK ([0-9\.])* ([0-9\.])* .*", result) |
| |
| self.keyvals['connect_config_s'] = result_times.group(1) |
| self.keyvals['connect_assoc_s'] = result_times.group(2) |
| |
| print "%s: %s" % (self.name, result) |
| |
| # fetch IP address of wireless device |
| self.client_wifi_ip = self.__get_ipaddr(self.client, self.client_wlanif) |
| logging.info("%s: client WiFi-IP is %s", self.name, self.client_wifi_ip) |
| # TODO(sleffler) not right for non-mac80211 devices |
| # TODO(sleffler) verify debugfs is mounted @ /sys/kernel/debug |
| self.client_debugfs_path = "/sys/kernel/debug/ieee80211/%s/netdev:%s" \ |
| % ("phy0", self.client_wlanif) |
| |
| |
| def disconnect(self, params): |
| """ Disconnect previously connected client """ |
| |
| self.client_ping_bg_stop({}) |
| |
| # copy site_wlan_disconnect.py over |
| script_name = 'site_wlan_disconnect.py' |
| script_client_file = self.client.get_tmp_dir() + '/' + script_name |
| self.client.send_file( |
| os.path.dirname(os.path.realpath(__file__)) + '/' + script_name, |
| script_client_file, |
| delete_dest=True) |
| |
| result = self.client.run('python "%s" "%s" "%d"' % |
| (script_client_file, |
| params.get('ssid', self.defssid), |
| params.get('wait_timeout', 15))).stdout.rstrip() |
| |
| print "%s: %s" % (self.name, result) |
| |
| |
| def client_powersave_on(self, params): |
| """ Enable power save operation """ |
| self.client.run("iwconfig %s power on" % self.client_wlanif) |
| |
| |
| def client_powersave_off(self, params): |
| """ Disable power save operation """ |
| self.client.run("iwconfig %s power off" % self.client_wlanif) |
| |
| |
| def __client_check(self, param, want): |
| """ Verify negotiated station mode parameter """ |
| result = self.client.run("cat '%s/%s'" % |
| (self.client_debugfs_path, param)) |
| got = result.stdout.rstrip() # NB: chop \n |
| if got != want: |
| raise error.TestFail("client_check_%s: wanted %s got %s", |
| param, want, got) |
| |
| |
| def client_check_bintval(self, params): |
| """ Verify negotiated beacon interval """ |
| self.__client_check("beacon_int", params[0]) |
| |
| |
| def client_check_dtimperiod(self, params): |
| """ Verify negotiated DTIM period """ |
| self.__client_check("dtim_period", params[0]) |
| |
| |
| def client_check_rifs(self, params): |
| """ Verify negotiated RIFS setting """ |
| self.__client_check("rifs", params[0]) |
| |
| |
| def client_check_shortgi20(self, params): |
| """ Verify negotiated Short GI setting """ |
| self.__client_check("sgi20", params[0]) |
| |
| |
| def client_check_shortgi40(self, params): |
| """ Verify negotiated Short GI setting """ |
| self.__client_check("sgi40", params[0]) |
| |
| |
| def client_check_shortslot(self, params): |
| """ Verify negotiated Short Slot setting """ |
| self.__client_check("short_slot", params[0]) |
| |
| |
| def client_check_protection(self, params): |
| """ Verify negotiated CTS protection setting """ |
| self.__client_check("cts_prot", params[0]) |
| |
| |
| def client_monitor_start(self, params): |
| """ Start monitoring system events """ |
| raise NotImplemented("client_monitor_start") |
| |
| |
| def client_monitor_stop(self, params): |
| """ Stop monitoring system events """ |
| raise NotImplemented("client_monitor_stop") |
| |
| |
| def client_check_event_mic(self, params): |
| """ Check for MIC error event """ |
| raise NotImplemented("client_check_event_mic") |
| |
| |
| def client_check_event_countermeasures(self, params): |
| """ Check for WPA CounterMeasures event """ |
| raise NotImplemented("client_check_event_countermeasures") |
| |
| |
| def sleep(self, params): |
| time.sleep(float(params['time'])) |
| |
| |
| def __unreachable(self, method): |
| logging.info("%s: SKIP step %s; server is unreachable", |
| self.name, method) |
| |
| |
| 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 """ |
| if self.server is None: |
| self.__unreachable("server_ping") |
| return |
| ping_ip = params.get('ping_ip', self.client_wifi_ip) |
| count = params.get('count', 10) |
| # set timeout for 3s / ping packet |
| result = self.server.run("ping %s %s" % \ |
| (self.__ping_args(params), ping_ip), timeout=3*int(count)) |
| |
| self.__print_pingstats("server_ping ", |
| self.__get_pingstats(result.stdout)) |
| |
| |
| def server_ping_bg(self, params): |
| """ Ping the client from the server """ |
| if self.server is None: |
| self.__unreachable("server_ping_bg") |
| return |
| ping_ip = params.get('ping_ip', self.client_wifi_ip) |
| cmd = "ping %s %s" % (self.__ping_args(params), ping_ip) |
| self.ping_thread = HelperThread(self.server, cmd) |
| self.ping_thread.start() |
| |
| |
| def server_ping_bg_stop(self, params): |
| if self.server is None: |
| self.__unreachable("server_ping_bg_stop") |
| return |
| if self.ping_thread is not None: |
| self.server.run("pkill ping") |
| self.ping_thread.join() |
| self.ping_thread = None |
| |
| |
| def __run_iperf(self, client_ip, server_ip, params): |
| template = ''.join(["job.run_test('iperf', \ |
| server_ip='%s', client_ip='%s', role='%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'] |
| |
| # add a tag to distinguish runs when multiple tests are run |
| if 'tag' in params: |
| tag = params['tag'] |
| elif 'udp' in params: |
| tag = "udp" |
| else: |
| tag = "tcp" |
| if 'bidir' in params: |
| tag += "_bidir" |
| template += ", tag='%s'" % tag |
| |
| template += ")" |
| |
| client_control_file = template % (server_ip, client_ip, 'client') |
| client_command = subcommand.subcommand(self.client_at.run, |
| [client_control_file, self.client.hostname]) |
| cmds = [client_command] |
| |
| if self.server is None: |
| logging.info("%s: iperf %s => (%s)", |
| self.name, client_ip, server_ip) |
| else: |
| server_control_file = template % (server_ip, client_ip, 'server') |
| server_command = subcommand.subcommand(self.server_at.run, |
| [server_control_file, self.server.hostname]) |
| cmds.append(server_command) |
| |
| logging.info("%s: iperf %s => %s", self.name, client_ip, server_ip) |
| |
| subcommand.parallel(cmds) |
| |
| |
| 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 """ |
| if self.server is None: |
| self.__unreachable("server_iperf") |
| return |
| self.__run_iperf(self.server_wifi_ip, self.client_wifi_ip, params) |
| |
| |
| def __run_netperf(self, client_ip, server_ip, params): |
| template = "job.run_test('" |
| if self.server is None: |
| template += "network_netperf2" |
| else: |
| template += "netperf2" |
| template += "', 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'] |
| |
| # add a tag to distinguish runs when multiple tests are run |
| if 'tag' in params: |
| template += ", tag='%s'" % params['tag'] |
| elif 'test' in params: |
| template += ", tag='%s'" % params['test'] |
| |
| template += ")" |
| |
| client_control_file = template % (server_ip, client_ip, 'client') |
| client_command = subcommand.subcommand(self.client_at.run, |
| [client_control_file, self.client.hostname]) |
| cmds = [client_command] |
| |
| if self.server is None: |
| logging.info("%s: netperf %s => (%s)", |
| self.name, client_ip, server_ip) |
| else: |
| server_control_file = template % (server_ip, client_ip, 'server') |
| server_command = subcommand.subcommand(self.server_at.run, |
| [server_control_file, self.server.hostname]) |
| cmds.append(server_command) |
| |
| logging.info("%s: netperf %s => %s", |
| self.name, client_ip, server_ip) |
| |
| subcommand.parallel(cmds) |
| |
| |
| 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 """ |
| if self.server is None: |
| self.__unreachable("server_netperf") |
| return |
| self.__run_netperf(self.server_wifi_ip, self.client_wifi_ip, params) |
| |
| |
| 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) |
| |
| |
| 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, *args): |
| """ |
| 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 any(fnmatch.fnmatch(file, pat) for pat in args): |
| 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) |
| |
| |
| def read_wifi_testbed_config(file, client_addr=None, server_addr=None, |
| router_addr=None): |
| # read configuration file |
| fd = open(file) |
| config = eval(fd.read()) |
| |
| # client must be reachable on the control network |
| client = config['client'] |
| if client_addr is not None: |
| client['addr'] = client_addr; |
| |
| # router must be reachable on the control network |
| router = config['router'] |
| if router_addr is not None: |
| server['router'] = router_addr; |
| |
| server = config['server'] |
| if server_addr is not None: |
| server['addr'] = server_addr; |
| # TODO(sleffler) check for wifi_addr when no control address |
| |
| # tag jobs w/ the router's address on the control network |
| config['tagname'] = router['addr'] |
| |
| return config |