blob: 0e49283c53aa91d5765491a489ce65ec4d9897f4 [file] [log] [blame]
#!/usr/bin/python2.7
"""sample: measure wireless performance and write a report to the filesystem."""
import atexit
import collections
import functools
import logging
import os
import pipes
import platform
import subprocess
import sys
import time
from fabric import api
from fabric import network
from fabric import utils
import ifstats
import ifstats.skids as ifstats_skids
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= interface to use for outgoing connections
m,monitor try to monitor wireless traffic using `tcpdump`
M,monitor-interface= override default interface for monitoring traffic.
no-filter don't attempt to filter packets seen on monitor interface
r,remote= comma-separated list of remote hosts to run tests on
S,skid= IP address of the skid used to reach the network
t,time= length of time in seconds to run isostream test for [50]
"""
class FabricLoggingHandler(logging.Handler):
def emit(self, record):
msg = self.format(record)
if record.levelno >= logging.ERROR:
utils.error(msg)
elif record.levelno == logging.WARNING:
utils.warn(msg)
else:
utils.puts(msg)
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():
# Structured logging. Helps to pick up Paramiko (SSH transport layer) errors.
root = logging.getLogger()
root.setLevel(logging.INFO)
root.addHandler(FabricLoggingHandler())
o = options.Options(optspec)
(opt, _, extra) = o.parse(sys.argv[1:])
if extra:
o.fatal('did not understand supplied extra arguments.')
# Build a directory for the report to live in.
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
if not os.path.exists(dest_dir):
os.makedirs(dest_dir)
logging.info('Report being written to %s', 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.
cache = collections.defaultdict(dict)
local_run = functools.partial(_Run, local=True)
# always gather network statistics for the system we're on
system = platform.system()
lifs = ifstats.InterfaceStats(system=system, runner=local_run)
for host, output in api.execute(lifs.Gather).items():
cache[host].update(output)
lresults = ifstats.Parse(system, cache['<local-only>'])
bssid = lresults.get('link', {}).get('BSSID')
if opt.skid:
logging.info('Getting stats from skid %s', opt.skid)
try:
status_wireless = ifstats_skids.GetStatusWireless(opt.skid)
cache[opt.skid]['status_wireless'] = status_wireless
except IOError as e:
logging.warning('Got IOError while connecting to skid %s: %s',
opt.skid, e)
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',
})
hosts = opt.remote.split(',')
wd = api.execute(_SetupTestHost, hosts=hosts).values()[0]
atexit.register(api.execute, _CleanupTestHost, wd, hosts=hosts)
ifsystem = 'Linux'
run = functools.partial(_Run, dirname=wd, local=False)
# Gather network statistics from remote system
ifs = ifstats.InterfaceStats(system=ifsystem, runner=run,
interface=opt.interface)
for host, output in api.execute(ifs.Gather, hosts=hosts).items():
cache[host].update(output)
# try to compute a bind address for iperf automatically.
results = ifstats.Parse(system, cache[opt.remote])
addr = results.get('addr', {}).get(opt.interface, '')
else:
ifsystem = system
run = local_run
hosts = []
# try to compute a bind address for iperf automatically.
addr = lresults.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:
defaults = {
# on a modern MacBook, en0 is the AirPort and can both monitor and send
'Darwin': 'en0',
# on Linux, separate client and monitor interfaces are needed; and the
# monitor interface must be manually created
'Linux': 'moni0', # from the `iw` documentation
}
if opt.monitor_interface:
monif = opt.monitor_interface
else:
monif = defaults.get(system)
if not monif:
logging.error('Cannot gather tcpdump: no monitor interface specified '
'and autodetect failed.')
tcpdump_proc, tcpdump_stderr = tcpdump.Tcpdump(monif, bssid, dest_dir,
opt.filter)
logging.info('Gathering tcpdump in background as %d', 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):
for host, output in api.execute(ips.RunTestSeries, opt.destination,
hosts=hosts).items():
cache[host].update(output)
# `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]):
for host, output in api.execute(isostream.RunIsostreamTest, run,
opt.destination, time=opt.time,
hosts=hosts).items():
cache[host]['isostream'] = output
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
# TODO(willangley): write out correctly if more than one host
for host, items in cache.items():
for name, value in items.items():
with open(os.path.join(dest_dir, name), 'w+b') as outfile:
outfile.write(value)
# On a Mac, announce the end of tests.
subprocess.check_call(['which say && say "Done"'], shell=True)
if __name__ == '__main__':
main()