| #!/usr/bin/env python |
| |
| """sample: measure wireless performance and write a report to the filesystem.""" |
| |
| import atexit |
| import functools |
| import os |
| import pipes |
| import platform |
| import subprocess |
| import sys |
| import time |
| |
| from fabric import api |
| from fabric import network |
| |
| import ifstats |
| import iperf |
| import isostream |
| import options |
| import tcpdump |
| |
| optspec = """ |
| sample [options...] |
| -- |
| B,bind= interface IP to bind during iperf runs |
| d,destination= host to run tests against [192.168.1.143] |
| s,steps= number of steps test was run from [10] |
| j,journal= append to journal tracking a series of test runs |
| i,interface= wireless interface to use for outgoing connections |
| m,monitor= wireless monitor interface to use |
| no-filter don't attempt to filter packets seen on monitor interface |
| r,remote= remote host to run tests on |
| t,time= length of time in seconds to run isostream test for [50] |
| """ |
| |
| |
| def _Run(cmd_or_args, dirname='.', local=False): |
| if isinstance(cmd_or_args, list): |
| cmd = ' '.join([pipes.quote(arg) for arg in cmd_or_args]) |
| else: |
| cmd = cmd_or_args |
| |
| if local: |
| with api.lcd(dirname): |
| return api.local(cmd, capture=True) |
| else: |
| with api.cd(dirname): |
| return api.run(cmd) |
| |
| |
| def _SetupTestHost(): |
| # work around current embedded image's lack of mktemp(1). |
| wd = api.run('python -c "import tempfile; print tempfile.mkdtemp()"') |
| with api.cd(wd): |
| for script in ['timeout']: |
| api.put(script, script, mode=0755) |
| return wd |
| |
| |
| def _CleanupTestHost(dirname): |
| api.run('rm -r {}'.format(pipes.quote(dirname))) |
| network.disconnect_all() |
| |
| |
| def main(): |
| system = platform.system() |
| defaults = { |
| # on a modern MacBook, en0 is the AirPort and can monitor and send at once |
| 'Darwin': { |
| 'monitor': 'en0' |
| }, |
| # on Linux, separate client and monitor interfaces are needed; and the |
| # monitor interface must be manually created |
| 'Linux': { |
| 'monitor': 'moni0' # from the `iw` documentation |
| }, |
| } |
| |
| o = options.Options(optspec) |
| (opt, _, extra) = o.parse(sys.argv[1:]) |
| if extra: |
| o.fatal('did not understand supplied extra arguments.') |
| |
| if not defaults.get(system): |
| raise OSError('Running on unsupported system {0}; ' |
| 'supported systems are {1}'.format(system, |
| ' '.join(defaults.keys()))) |
| |
| # Pick the report name before we run it, so it can be consistent across |
| # multiple systems. |
| report_name = 'wifi-{}-{:04}'.format(time.time(), opt.steps) |
| |
| if opt.journal: |
| dest_dir = os.path.join(os.path.dirname(opt.journal), report_name) |
| else: |
| dest_dir = report_name |
| |
| print 'Report being written to', dest_dir |
| if not os.path.exists(dest_dir): |
| os.makedirs(dest_dir) |
| |
| # we run diagnostics, write their output to files, and gather the files into |
| # a report that we present at the end of the run. |
| # |
| # entries collects Entry objects that have not been written to disk yet. |
| execute_args = {} |
| if opt.remote: |
| # update Fabric env for embedded systems |
| api.env.update({ |
| 'always_use_pty': False, |
| 'key_filename': os.path.expanduser('~/.ssh/bruno-sshkey'), |
| 'user': 'root', |
| 'shell': 'sh -l -c', |
| }) |
| execute_args['host'] = opt.remote |
| |
| wd = api.execute(_SetupTestHost, **execute_args).values()[0] |
| atexit.register(api.execute, _CleanupTestHost, wd, **execute_args) |
| |
| ifsystem = 'Linux' |
| run = functools.partial(_Run, dirname=wd, local=False) |
| else: |
| ifsystem = system |
| run = functools.partial(_Run, local=True) |
| |
| ifs = ifstats.InterfaceStats(system=ifsystem, |
| runner=run, |
| interface=opt.interface) |
| |
| # since we're only executing over one host, ignore the return from `execute` |
| # that says which host it was for now. |
| cache = api.execute(ifs.Gather, **execute_args).values()[0] |
| results = ifstats.Parse(ifsystem, cache) |
| |
| bssid = results['link']['BSSID'] |
| addr = results.get('addr', {}).get(opt.interface, '') |
| |
| # because ip addr usually includes a subnet mask, which will prevent iperf |
| # from binding to the address |
| mask = addr.find('/') |
| if mask > -1: |
| addr = addr[:mask] |
| |
| if opt.monitor: |
| tcpdump_proc, tcpdump_stderr = tcpdump.tcpdump(bssid, opt.monitor, dest_dir, |
| opt.filter) |
| print 'Gathering tcpdump in background as', tcpdump_proc.pid |
| |
| if opt.bind and not addr: |
| addr = opt.bind |
| |
| ips = iperf.Iperf(runner=run, bind=addr) |
| |
| # `iperf` returns 56 if it can't reach the server, or 57 if it doesn't receive |
| # a final report from it on Linux; don't abort in these cases |
| with api.settings(warn_only=True): |
| cache.update( |
| api.execute(ips.RunTestSeries, opt.destination, |
| **execute_args).values()[0] |
| ) |
| |
| # `isostream` won't end on its own, so we wrap it with `timeout` and accept a |
| # return code of 124 (timed out) as well. |
| with api.settings(ok_ret_codes=[0, 124]): |
| cache['isostream'] = api.execute(isostream.RunIsostreamTest, run, |
| opt.destination, time=opt.time, |
| **execute_args).values()[0] |
| |
| if opt.monitor: |
| try: |
| tcpdump_proc.terminate() |
| except OSError: |
| subprocess.check_call(['sudo', 'kill', str(tcpdump_proc.pid)]) |
| |
| tcpdump_stderr.flush() |
| tcpdump_stderr.close() |
| |
| if opt.journal: |
| with open(opt.journal, 'a') as journal: |
| print >> journal, report_name |
| |
| for name, value in cache.items(): |
| with open(os.path.join(dest_dir, name), 'w+b') as outfile: |
| outfile.write(value) |
| |
| |
| if __name__ == '__main__': |
| main() |
| |