blob: 83b4e7c3d5aff14cc4a63629765e6c705bcdb8ed [file] [log] [blame]
#!/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()