00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014 import subprocess
00015 import os
00016 import os.path
00017 import atexit
00018 import select
00019 import time
00020 import sys
00021 import threading
00022 import signal
00023 import ipaddr
00024 import network_monitor_udp.udpmoncli
00025 import pythonwifi.iwlibs
00026 import traceback
00027 import errno
00028 import yaml
00029 import math
00030 import inspect
00031 import fcntl
00032 import socket
00033 import logging
00034 import logging.handlers
00035
00036 null_file = open('/dev/null')
00037
00038 LOCAL_RULE=50
00039 FIRST_IFACE_RULE=100
00040 TUNNEL_RULE2=75
00041 TUNNEL_RULE=150
00042 BLACKHOLE_BASESTATION_RULE=175
00043 DEFAULT_RULE=200
00044 BLOCK_NON_TUNNEL_RULE=250
00045
00046 NOLINK = -2
00047 NOIP = -1
00048 NOCHECK = 0
00049
00050 STATUSES = {
00051 NOLINK : "Establishing Link",
00052 NOIP : "Getting an IP address",
00053 NOCHECK : "Ready"
00054 }
00055
00056
00057
00058 class LoggerStream:
00059 def __init__(self, func):
00060 self.lock = threading.Lock()
00061 self.buffer = ""
00062 self.func = func
00063
00064 def write(self, str):
00065 with self.lock:
00066 self.buffer += str
00067 while True:
00068 pos = self.buffer.find('\n')
00069 if pos == -1:
00070 break
00071 self.func(self.buffer[0:pos])
00072 self.buffer = self.buffer[pos+1:]
00073
00074 def flush(self):
00075 if self.buffer:
00076 with self.lock:
00077 self.func(self.buffer)
00078
00079 logdir = '/var/log/roam'
00080
00081 try:
00082 os.makedirs(logdir)
00083 except:
00084 pass
00085 logfilecount = 10
00086
00087 file_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
00088
00089 console_logger = logging.getLogger('console')
00090 console_logger.setLevel(logging.DEBUG)
00091 strategy_logger = logging.getLogger('console.strategy')
00092 strategy_logger.setLevel(logging.DEBUG)
00093
00094 console_handler = logging.StreamHandler(sys.stdout)
00095 console_handler.setLevel(logging.DEBUG)
00096 console_formatter = logging.Formatter('%(message)s')
00097 console_handler.setFormatter(console_formatter)
00098 console_logger.addHandler(console_handler)
00099
00100 console_file_handler = logging.handlers.TimedRotatingFileHandler(os.path.join(logdir,'console-output.log'), when='midnight', backupCount=logfilecount)
00101 console_file_handler.setFormatter(file_formatter)
00102 console_file_handler.setLevel(logging.DEBUG)
00103 console_logger.addHandler(console_file_handler)
00104
00105 all_logger = logging.getLogger('')
00106 all_logger_handler = logging.handlers.TimedRotatingFileHandler(os.path.join(logdir,'all.log'), when='midnight', backupCount=logfilecount)
00107 all_logger_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
00108 all_logger_handler.setFormatter(all_logger_formatter)
00109 all_logger_handler.setLevel(logging.DEBUG)
00110 all_logger.addHandler(all_logger_handler)
00111
00112
00113 sys.stdout = LoggerStream(console_logger.info)
00114 sys.stderr = LoggerStream(console_logger.error)
00115 strategy_str = LoggerStream(strategy_logger.info)
00116
00117 def log_time_string(t):
00118 return time.strftime("%a, %d %b %Y %H:%M:%S.", time.localtime(t))+("%.6f"%t).split('.')[1]
00119
00120 def flushiprule(rule_prio, leave_one = False):
00121 rules = RunCommand('ip', 'rule').stdout.split('\n')
00122 prefix = "%i:"%rule_prio
00123 count = 0
00124 for line in rules:
00125 if line.startswith(prefix):
00126 count += 1
00127 if leave_one:
00128 count -= 1
00129 for i in range(0, count):
00130 System('ip rule del priority %i'%rule_prio)
00131
00132 def safe_shutdown(method, *args, **nargs):
00133 try:
00134 method(*args, **nargs)
00135 except Exception, e:
00136 print "Caught exception while shutting down instance.", method
00137 traceback.print_exc(10)
00138 print
00139
00140 class System:
00141 def __init__(self, arg):
00142
00143 self.errcode = subprocess.call(arg.split(), stdout = null_file, stderr = null_file, close_fds = True)
00144
00145
00146 class RunCommand:
00147 def __init__(self, *args):
00148 proc = subprocess.Popen(list(args), stdout = subprocess.PIPE, stderr = subprocess.PIPE, close_fds = True)
00149 (self.stdout, self.stderr) = proc.communicate()
00150
00151 class CommandWithOutput(threading.Thread):
00152 def __init__(self, args, name):
00153 self.restart_delay = 0.2
00154 threading.Thread.__init__(self, name = name)
00155
00156
00157
00158
00159
00160
00161
00162
00163
00164 self.logger = logging.getLogger(name)
00165 self.console_logger = logging.getLogger('console.%s'%name)
00166 logger_handler = logging.handlers.TimedRotatingFileHandler(os.path.join(logdir,'%s.log'%name), when='midnight', backupCount=logfilecount)
00167 logger_handler.setFormatter(file_formatter)
00168 self.logger.addHandler(logger_handler)
00169 self.console_logger.addHandler(logger_handler)
00170 self.logger.setLevel(logging.DEBUG)
00171 self.console_logger.setLevel(logging.DEBUG)
00172 logger_handler.setLevel(logging.DEBUG)
00173 self.proc_args = args
00174 self.start_proc()
00175 self.running = True
00176 self.start()
00177
00178 def start_proc(self):
00179 try:
00180 self.proc = subprocess.Popen(self.proc_args, stdout = subprocess.PIPE, stderr = subprocess.STDOUT, close_fds = True)
00181 flags = fcntl.fcntl(self.proc.stdout, fcntl.F_GETFL)
00182 fcntl.fcntl(self.proc.stdout, fcntl.F_SETFL, flags| os.O_NONBLOCK)
00183 self.child_restart()
00184 except OSError:
00185 self.console_logger.fatal("Error trying to run: %s"%(" ".join(self.proc_args)))
00186 raise KeyboardInterrupt
00187
00188 def child_restart(self):
00189 pass
00190
00191 def run(self):
00192 read_buffer = {}
00193 try:
00194 while True:
00195 (rd, wr, err) = select.select([self.proc.stdout], [], [], 0.2)
00196 if not self.running:
00197
00198 try:
00199 self.proc.send_signal(signal.SIGINT)
00200 except OSError, e:
00201 if str(e).find('[Errno 3]') == -1:
00202 raise
00203
00204 try:
00205 self.proc.communicate()
00206 except IOError:
00207 pass
00208
00209 return
00210 for fd in rd:
00211
00212 try:
00213 newdata = fd.read()
00214 except IOError:
00215 newdata = ""
00216
00217 if len(newdata) == 0:
00218 self.proc.kill()
00219 self.proc.communicate()
00220 time.sleep(self.restart_delay)
00221 if not self.running:
00222 return
00223 self.console_logger.info("Process died, restarting: %s"%(" ".join(self.proc_args)))
00224 self.start_proc()
00225 continue
00226
00227 if fd in read_buffer:
00228 newdata = read_buffer[fd] + newdata
00229 while True:
00230 splitpos = newdata.find('\n')
00231 if splitpos == -1:
00232 read_buffer[fd] = newdata
00233 break
00234 line = newdata[0:splitpos]
00235 newdata = newdata[splitpos+1:]
00236
00237 self.logger.info(line)
00238
00239
00240
00241
00242
00243
00244 try:
00245 self.got_line(line)
00246 except Exception, e:
00247 self.console_logger.fatal("Caught exception in CommandWithOutput.run: %s"%str(e))
00248 raise
00249 except:
00250 traceback.print_exc(10)
00251 print
00252 self.console_logger.fatal("Command with output triggering shutdown after exception.")
00253 os.kill(os.getpid(), signal.SIGINT)
00254 raise
00255
00256 def shutdown(self):
00257 self.running = False
00258
00259
00260
00261
00262
00263
00264
00265 class WpaSupplicant(CommandWithOutput):
00266 def __init__(self, iface, config):
00267 self.iface = iface
00268 script = os.path.join(os.path.dirname(__file__), 'wpa_supplicant.sh')
00269 CommandWithOutput.__init__(self, [script, '-i', iface, '-c', config, '-C', '/var/run/wpa_supplicant', '-dd'], "wpa_supplicant_"+iface)
00270 self.restart_delay = 1
00271
00272 def got_line(self, line):
00273 pass
00274
00275 def command(self, cmd):
00276 System('wpa_cli -p /var/run/wpa_supplicant -i %s %s'%(self.iface, cmd))
00277
00278 def restart(self):
00279 print >> sys.stderr, "Restarting supplicant on %s"%self.iface
00280 try:
00281 self.proc.send_signal(signal.SIGINT)
00282 except OSError, e:
00283 if str(e).find('[Errno 3]') == -1:
00284 raise
00285
00286 class DhcpClient(CommandWithOutput):
00287 def __init__(self, iface, bound_callback = None, deconfig_callback = None, leasefail_callback = None):
00288 self.iface = iface
00289 self.bound_callback = bound_callback
00290 self.deconfig_callback = deconfig_callback
00291 self.leasefail_callback = leasefail_callback
00292 script = os.path.join(os.path.dirname(__file__), 'udhcpc_echo.sh')
00293 udhcpc_script = os.path.join(os.path.dirname(__file__), 'udhcpc.sh')
00294 CommandWithOutput.__init__(self, [udhcpc_script, '-s', script, '-i', iface, '-f'], "udhcpc_"+iface)
00295 self.restart_delay = 0
00296
00297 def got_line(self, line):
00298 boundprefix = "UDHCPC: bound "
00299 if line.startswith(boundprefix):
00300 [iface, ip, netmask, gateway] = line[len(boundprefix):].split()
00301 if self.bound_callback is not None:
00302 self.bound_callback(iface, ip, netmask, gateway)
00303
00304 deconfigprefix = "UDHCPC: deconfig "
00305 if line.startswith(deconfigprefix):
00306 [iface] = line[len(deconfigprefix):].split()
00307 if self.deconfig_callback is not None:
00308 self.deconfig_callback(iface)
00309
00310 leasefailprefix = "UDHCPC: leasefail "
00311 if line.startswith(leasefailprefix):
00312 [iface] = line[len(leasefailprefix):].split()
00313 if self.leasefail_callback is not None:
00314 self.leasefail_callback(iface)
00315
00316 def renew(self):
00317
00318
00319
00320 self.proc.send_signal(signal.SIGINT)
00321
00322 def release(self):
00323 try:
00324 self.proc.send_signal(signal.SIGUSR2)
00325 except OSError, e:
00326 if str(e).find('[Errno 3]') == -1:
00327 raise
00328
00329 class NetlinkMonitor(CommandWithOutput):
00330 def __init__(self):
00331 self.lock = threading.RLock()
00332 self.link_callbacks = {}
00333 self.addr_callbacks = {}
00334 self.addr_state = {}
00335 self.link_state = {}
00336 self.cur_iface = None
00337 self.deleted = None
00338 CommandWithOutput.__init__(self, ['ip', 'monitor', 'link', 'addr'], 'ip_monitor')
00339
00340
00341 def child_restart(self):
00342 time.sleep(0.2)
00343 current_state = RunCommand('ip', 'addr')
00344 with self.lock:
00345 old_cur_iface = self.cur_iface
00346 old_deleted = self.deleted
00347 for line in current_state.stdout.split('\n'):
00348 self.got_line(line)
00349 self.deleted = old_deleted
00350 self.cur_iface = old_cur_iface
00351
00352 def register_link(self, iface, callback):
00353 if ':' in iface:
00354 iface = iface.split(':')[0]
00355 if iface not in self.link_callbacks:
00356 self.link_callbacks[iface] = []
00357 with self.lock:
00358 self.link_callbacks[iface].append(callback)
00359 if iface not in self.link_state:
00360 self.link_state[iface] = False
00361 callback(self.link_state[iface])
00362
00363 def register_addr(self, iface, callback):
00364 print "register_addr", iface
00365 if iface not in self.addr_callbacks:
00366 self.addr_callbacks[iface] = []
00367 with self.lock:
00368 self.addr_callbacks[iface].append(callback)
00369 if iface not in self.addr_state:
00370 self.addr_state[iface] = [None, None]
00371 callback(*self.addr_state[iface])
00372
00373 def got_line(self, line):
00374 with self.lock:
00375 try:
00376
00377
00378
00379
00380 if len(line) == 0 or (line[0] == ' ' and self.cur_iface == None):
00381 return
00382 tokens = line.rstrip().split()
00383 if line[0] != ' ':
00384 if tokens[0] == 'Deleted':
00385 self.deleted = True
00386 tokens.pop(0)
00387 else:
00388 self.deleted = False
00389 self.cur_iface = tokens[1].rstrip(':')
00390 tokens.pop(0)
00391 tokens.pop(0)
00392
00393
00394 try:
00395 state_idx = tokens.index('state')
00396 state = tokens[state_idx + 1]
00397 link_state = state != 'DOWN'
00398 if self.cur_iface not in self.link_state or self.link_state[self.cur_iface] != link_state:
00399 self.link_state[self.cur_iface] = link_state
00400 if self.cur_iface in self.link_callbacks:
00401 for callback in self.link_callbacks[self.cur_iface]:
00402 callback(link_state)
00403 print "********************Link change", self.cur_iface, link_state
00404
00405
00406 except ValueError:
00407
00408 pass
00409
00410
00411
00412 try:
00413 addr_idx = tokens.index('inet')
00414 if self.deleted:
00415 addr_state = [None, None]
00416 else:
00417 addr_state = tokens[addr_idx + 1].split('/')
00418 full_iface = tokens[-1]
00419 if full_iface not in self.addr_state or self.addr_state[full_iface] != addr_state:
00420 print "********************Address change", full_iface, addr_state
00421 self.addr_state[full_iface] = addr_state
00422 if full_iface in self.addr_callbacks:
00423 for callback in self.addr_callbacks[full_iface]:
00424 callback(*addr_state)
00425
00426
00427 except ValueError:
00428
00429 pass
00430 except Exception, e:
00431 print "Caught exception in NetlinkMonitor.run:", e
00432 traceback.print_exc(10)
00433 print
00434
00435 class NetworkConnection:
00436 def __init__(self, name, iface, config, tableid, pingtarget):
00437 self.timeout_time = 1e1000
00438 self.status = NOLINK
00439 if 'name' in config:
00440 self.name = config['name']
00441 else:
00442 self.name = name
00443 self.iface = iface
00444 self.tableid = tableid
00445 self.shutting_down = False
00446 self.bssid = "NoLink"
00447 self.priority = config['priority'] if 'priority' in config else 0
00448
00449 self.udp_monitor = network_monitor_udp.udpmoncli.MonitorClient([.2], pingtarget, 20, 32, True)
00450 netlink_monitor.register_addr(self.iface, self.addrchange)
00451
00452 def addrchange(self, addr, netbits):
00453 if addr:
00454 self.status = NOCHECK
00455
00456
00457
00458
00459
00460 self.set_routes(addr, netbits)
00461 try:
00462 print 'start_monitor', addr
00463 self.udp_monitor.start_monitor((addr, 0))
00464 except:
00465 pass
00466 elif self.status == NOCHECK:
00467 print 'stop_monitor'
00468 self.status = NOIP
00469 self.udp_monitor.stop_monitor()
00470
00471 def update1(self):
00472 self.monitor_output = self.udp_monitor.get_smart_bins(1)
00473 (bins, latency1, latency2) = self.monitor_output
00474
00475 self.diags = []
00476
00477 self.diags.append(("Link status", (STATUSES[self.status] if self.status in STATUSES else "unknown")))
00478
00479 self.diags.append(("Packets under 200ms latency (%)", 100 * bins[0]))
00480 self.diags.append(("Average round trip latency (ms)", "%.1f"%(latency1*1000)))
00481 self.diags.append(("Average latency for packets under 200ms (ms)", "%.1f"%(latency2*1000)))
00482
00483 return (bins, latency1, latency2)
00484
00485 def update2(self):
00486 (bins, latency1, latency2) = self.monitor_output
00487
00488 if self.status < 0:
00489 self.goodness = self.status
00490 self.reliability = self.status
00491 else:
00492 self.goodness = 100 * bins[0] - latency2
00493
00494 self.diags.append(("Goodness:", self.goodness))
00495 self.diags.append(("Reliability:", self.reliability))
00496
00497 if self.status < 0:
00498 self.diag_summary = STATUSES[self.status]
00499 else:
00500 self.diag_summary = "Connected (goodness %f, reliability %f)"%(self.goodness, self.reliability)
00501
00502 def shutdown(self):
00503 self.shutting_down = True
00504 safe_shutdown(self.udp_monitor.shutdown)
00505
00506 class StaticRoute(NetworkConnection):
00507 def __init__(self, route, config, tableid, pingtarget):
00508 self.gateway, iface = route.split('@')
00509 NetworkConnection.__init__(self, route, iface, config, tableid, pingtarget)
00510 netlink_monitor.register_link(self.iface, self.linkchange)
00511
00512 def increase_timeout(self, delay = 1e1000):
00513 pass
00514
00515 def set_timeout(self, delay):
00516 pass
00517
00518 def decrease_timeout(self, delay):
00519 pass
00520
00521 def set_routes(self, addr, netbits):
00522 flushiprule(self.tableid)
00523 System("ip rule add priority %i from %s table %i"%(self.tableid,addr,self.tableid))
00524 System("ip route flush table %i"%self.tableid)
00525 try:
00526 gatewayip = socket.gethostbyname(self.gateway)
00527 except socket.gaierror, e:
00528
00529 print >> sys.stderr, "Error resolving gateway host %s. Connection self.name will not work."%self.gateway
00530 return
00531 System("ip route add table %i default dev %s via %s src %s onlink"%(self.tableid, self.iface, gatewayip, addr))
00532 System("ip route add table %i %s dev %s src %s"%(self.tableid, gatewayip, self.iface, addr))
00533
00534 def linkchange(self, up):
00535 if not up:
00536 self.bssid = "NoLink"
00537 self.status = NOLINK
00538 else:
00539 self.bssid = "Wired"
00540 if self.status == NOLINK:
00541 self.status = NOIP
00542
00543 def update(self):
00544 self.update1()
00545 self.reliability = 100
00546 self.update2()
00547
00548 class DhcpInterface(NetworkConnection):
00549 def __init__(self, iface, config, tableid, pingtarget):
00550 self.is_bound = False
00551 NetworkConnection.__init__(self, iface, iface, config, tableid, pingtarget)
00552 self.start_over_timeout = 15
00553 self.dhcp_timeout = 7
00554 self.timeout_time = 0
00555 self.increase_timeout(self.start_over_timeout)
00556 self.timed_out_time = 0
00557
00558 self.dhclient = DhcpClient(iface, self.bound, self.deconfigured, self.leasefail)
00559 self.startover()
00560
00561 def update1(self):
00562 time_to_timeout = self.timeout_time - time.time()
00563 if time_to_timeout < 0:
00564 print "Interface", self.name, "timed out."
00565 self.timed_out_time = time.time()
00566 self.startover()
00567
00568 NetworkConnection.update1(self)
00569
00570 self.diags.append(("Time to interface timeout (s)", time_to_timeout))
00571
00572 def linkchange(self, up):
00573
00574 self.set_timeout(self.start_over_timeout)
00575
00576 if up:
00577 if self.status == NOLINK:
00578 self.bssid = "Wired"
00579 self.status = NOIP
00580 self.set_timeout(self.dhcp_timeout)
00581 self.dhclient.renew()
00582 else:
00583 if self.status != NOLINK:
00584 self.bssid = "NoLink"
00585 self.status = NOLINK
00586 self.udp_monitor.stop_monitor()
00587 self.dhclient.release()
00588 pass
00589
00590 def set_routes(self, addr, netbits):
00591 if not self.is_bound:
00592 return
00593 flushiprule(self.tableid)
00594
00595 System("ip rule add priority %i from %s table %i"%(self.tableid,addr,self.tableid))
00596 System("ip route flush table %i"%self.tableid)
00597 System("ip route add table %i default dev %s via %s src %s onlink"%(self.tableid, self.iface, self.gateway, addr))
00598 net = ipaddr.IPv4Network("%s/%s"%(addr,netbits))
00599
00600 print("ip route add table %i %s/%s dev %s src %s"%(self.tableid, net.network, net.prefixlen, self.iface, addr))
00601 System("ip route add table %i %s/%s dev %s src %s"%(self.tableid, net.network, net.prefixlen, self.iface, addr))
00602
00603 def bound(self, iface, ip, netmask, gateway):
00604 print "Bound!", self.status
00605
00606
00607 self.increase_timeout(self.start_over_timeout)
00608 self.is_bound = True
00609 self.ip = ip
00610 self.netmask = netmask
00611 self.gateway = gateway
00612
00613
00614
00615 System("ifconfig %s 0.0.0.0"%iface)
00616 net = ipaddr.IPv4Network("%s/%s"%(ip, netmask))
00617 System("ip addr add %s/%s dev %s"%(ip, net.prefixlen, iface))
00618
00619 System("arping -q -c 1 -A -I %s %s"%(iface,ip))
00620 ConfigureRpFilter(iface)
00621 print "Bound done!", self.status
00622
00623 def startover(self):
00624 flushiprule(self.tableid)
00625 System("ifconfig %s 0.0.0.0 down"%self.iface)
00626 System("ifconfig %s up"%self.iface)
00627 self.set_timeout(self.start_over_timeout)
00628
00629 def leasefail(self, iface):
00630 if self.status > NOLINK:
00631 self.startover()
00632
00633 def deconfigured(self, iface):
00634 if self.is_bound:
00635 self.is_bound = False
00636 flushiprule(self.tableid)
00637 System("ifconfig %s 0.0.0.0 down"%iface)
00638 System("ifconfig %s up"%iface)
00639
00640 def shutdown(self):
00641 NetworkConnection.shutdown(self)
00642 System("ifconfig %s down"%self.iface)
00643 safe_shutdown(self.dhclient.shutdown)
00644
00645 def decrease_timeout(self, delay):
00646
00647 self.timeout_time = min(time.time() + delay, self.timeout_time)
00648
00649 def increase_timeout(self, delay = 1e1000):
00650
00651 self.timeout_time = max(time.time() + delay, self.timeout_time)
00652
00653 def set_timeout(self, delay):
00654
00655 self.timeout_time = time.time() + delay
00656
00657 class WirelessInterface(DhcpInterface):
00658 def __init__(self, iface, config, tableid, pingtarget):
00659 self.startover_count = 0
00660
00661 DhcpInterface.__init__(self, iface, config, tableid, pingtarget)
00662
00663 self.wifi = pythonwifi.iwlibs.Wireless(iface)
00664 self.iwinfo = pythonwifi.iwlibs.WirelessInfo(iface)
00665
00666 self.supplicant = WpaSupplicant(iface, config['wpa_config'])
00667 self.initialized = True
00668 netlink_monitor.register_link(iface, self.linkchange)
00669
00670 def linkchange(self, up):
00671 if not self.initialized:
00672 return
00673
00674 if up:
00675 self.startover_count = 0
00676
00677
00678
00679 DhcpInterface.linkchange(self, up)
00680
00681 def startover(self):
00682 if self.startover_count > 1:
00683 self.startover_count = 0
00684 self.supplicant.restart()
00685 else:
00686 self.startover_count += 1
00687
00688 DhcpInterface.startover(self)
00689
00690 def update(self):
00691 self.update1()
00692
00693 try:
00694 self.essid = self.wifi.getEssid()
00695 self.diags.append(('ESSID', self.essid))
00696 except Exception, e:
00697 if self.status != NOLINK:
00698 traceback.print_exc(10)
00699 print
00700 self.diags.append(('ESSID', 'Error collecting data.'))
00701 self.essid = "###ERROR-COLLECTING-DATA###"
00702
00703 try:
00704 self.bssid = self.wifi.getAPaddr()
00705 self.diags.append(('BSSID', self.bssid))
00706 except Exception, e:
00707 if self.status != NOLINK:
00708 traceback.print_exc(10)
00709 print
00710 self.bssid = "00:00:00:00:00:00"
00711 self.diags.append(('BSSID', 'Error collecting data.'))
00712
00713 try:
00714 self.wifi_txpower = 10**(self.wifi.wireless_info.getTXPower().value/10.)
00715 self.diags.append(('TX Power (mW)', self.wifi_txpower))
00716 self.wifi_txpower = "%.1f mW"%self.wifi_txpower
00717 except Exception, e:
00718 if str(e).find("Operation not supported") == -1 and self.status != NOLINK:
00719 traceback.print_exc(10)
00720 print
00721 self.diags.append(('TX Power (mW)', 'Error collecting data.'))
00722 self.wifi_txpower = "unknown"
00723
00724 try:
00725 self.wifi_frequency = self.wifi.wireless_info.getFrequency().getFrequency()
00726 self.diags.append(('Frequency (Gz)', "%.4f"%(self.wifi_frequency/1e9)))
00727 except Exception, e:
00728 if self.status != NOLINK:
00729 traceback.print_exc(10)
00730 print
00731 self.wifi_frequency = 0
00732 self.diags.append(('Frequency', 'Error collecting data.'))
00733
00734 got_stats = False
00735 if self.status != NOLINK:
00736 try:
00737 stat, qual, discard, missed_beacon = self.wifi.getStatistics()
00738 max_quality = self.wifi.getQualityMax().quality
00739 quality = qual.quality * 100 / max_quality
00740 self.diags.append(('Quality', quality))
00741 self.diags.append(('Signal (dB)', qual.siglevel))
00742 self.diags.append(('Noise (dB)', qual.nlevel))
00743 self.diags.append(('SNR (dB)', qual.siglevel - qual.nlevel))
00744 self.wifi_signal = qual.siglevel
00745 self.wifi_noise = qual.nlevel
00746 self.wifi_quality = quality
00747 self.reliability = quality
00748 got_stats = True
00749 except Exception, e:
00750 print "Error getting wireless stats on interface %s: %s"%(self.iface, str(e))
00751
00752 if not got_stats:
00753
00754 print
00755 self.reliability = 0
00756 self.wifi_quality = -1
00757 self.wifi_noise = 1e1000
00758 self.wifi_signal = -1e1000
00759 for s in [ 'Quality', 'Signal', 'Noise' ]:
00760 if self.status != NOLINK:
00761 self.diags.append((s, 'Error collecting data.'))
00762 else:
00763 self.diags.append((s, 'Unknown'))
00764
00765 self.wifi_rate = None
00766 if self.status != NOLINK:
00767 try:
00768 self.wifi_rate = self.wifi.wireless_info.getBitrate().value
00769 except:
00770 pass
00771 if self.wifi_rate is None:
00772 try:
00773 self.wifi_rate = self.iwinfo.getBitrate().value
00774 except:
00775 pass
00776 if self.wifi_rate is not None:
00777 self.diags.append(('TX Rate (Mbps)', self.wifi_rate / 1e6))
00778 self.wifi_rate = self.wifi._formatBitrate(self.wifi_rate)
00779 else:
00780 if self.status != NOLINK:
00781 print "Unable to determine TX rate on interface", self.iface
00782 self.diags.append(('TX Rate (Mbps)', 'Error collecting data.'))
00783 else:
00784 self.diags.append(('TX Rate (Mbps)', 'Unknown'))
00785 self.wifi_rate = "Unknown"
00786
00787 self.update2()
00788
00789 def shutdown(self):
00790 DhcpInterface.shutdown(self)
00791 safe_shutdown(self.supplicant.shutdown)
00792
00793 class WiredInterface(DhcpInterface):
00794 def __init__(self, iface, config, tableid, pingtarget):
00795 self.initialized = False
00796 DhcpInterface.__init__(self, iface, config, tableid, pingtarget)
00797 self.initialized = True
00798 netlink_monitor.register_link(iface, self.linkchange)
00799
00800 def linkchange(self, up):
00801 if up != (self.status != NOLINK) and self.initialized and time.time() - self.timed_out_time > 10:
00802 os.system('beep')
00803 DhcpInterface.linkchange(self, up)
00804
00805 def update(self):
00806 self.update1()
00807 self.reliability = 100
00808 self.update2()
00809
00810 def shutdown(self):
00811 self.initialized = False
00812 DhcpInterface.shutdown(self)
00813
00814
00815
00816
00817
00818
00819
00820
00821
00822
00823
00824
00825
00826
00827
00828
00829
00830
00831
00832
00833
00834
00835
00836
00837
00838 class RoutingRules:
00839 def __init__(self, numinterfaces, localnets, tuniface, basestation):
00840 self.numinterfaces = numinterfaces
00841 self.tuniface = tuniface
00842 self.flushall()
00843
00844 System("ip rule add priority %i to %s blackhole"%(BLACKHOLE_BASESTATION_RULE, basestation))
00845
00846
00847
00848
00849
00850
00851 for net in localnets:
00852 System("ip rule add priority %i to %s lookup main"%(LOCAL_RULE, net))
00853 System("ip rule add priority %i lookup %i"%(DEFAULT_RULE,DEFAULT_RULE))
00854
00855
00856 netlink_monitor.register_link(self.tuniface, self.refresh)
00857
00858 def refresh(self, up):
00859 if up:
00860 System("ip route replace table %i default dev %s"%(DEFAULT_RULE, self.tuniface))
00861
00862 def flushall(self):
00863
00864 for i in range(0,self.numinterfaces):
00865 flushiprule(FIRST_IFACE_RULE + i)
00866 flushiprule(TUNNEL_RULE + i)
00867 flushiprule(DEFAULT_RULE)
00868 flushiprule(LOCAL_RULE)
00869 flushiprule(BLACKHOLE_BASESTATION_RULE)
00870
00871
00872
00873
00874
00875 for i in range(0,self.numinterfaces):
00876 self.flushtable(FIRST_IFACE_RULE + i)
00877 self.flushtable(DEFAULT_RULE)
00878
00879
00880 def flushtable(self, tab_id):
00881 System("ip route flush table %i"%tab_id)
00882
00883 def shutdown(self):
00884 self.flushall()
00885
00886 class ConfigureArp:
00887 def __init__(self):
00888 System("sysctl net.ipv4.conf.all.arp_filter=1")
00889
00890 class ConfigureRpFilter:
00891 def __init__(self, iface):
00892 System("sysctl net.ipv4.conf.%s.rp_filter=0"%iface)
00893
00894 class KillServices:
00895 def __init__(self):
00896 System("killall wpa_supplicant udhcpc 2> /dev/null")
00897
00898 class NetworkSelector:
00899 def __init__(self, config):
00900 interfaces_config = config['interfaces']
00901 self.base_station = socket.gethostbyname(config['base_station'])
00902 pingaddr = (self.base_station, int(config['ping_port']))
00903 self.tunnel_interface = config['tunnel_interface']
00904 local_networks = config['local_networks']
00905
00906 KillServices()
00907 global netlink_monitor
00908 netlink_monitor = NetlinkMonitor()
00909
00910 ConfigureArp()
00911 ConfigureRpFilter('all')
00912 self.routing_rules = RoutingRules(len(interfaces_config), local_networks, self.tunnel_interface, self.base_station)
00913 self.interfaces = []
00914 self.active_iface = -1
00915 self._tunnel_rules = {}
00916 self._set_tunnel_rule(TUNNEL_RULE, "blackhole")
00917
00918 i = -1
00919 for iface in interfaces_config:
00920 i += 1
00921 opts = interfaces_config[iface]
00922 type = opts['type']
00923 if type == "wireless":
00924 if 'wpa_config' not in opts and 'wpa_config' in config:
00925 opts['wpa_config'] = config['wpa_config']
00926 self.interfaces.append(WirelessInterface(iface, opts, FIRST_IFACE_RULE + i, pingaddr))
00927 elif type == "wired":
00928 self.interfaces.append(WiredInterface(iface, opts, FIRST_IFACE_RULE + i, pingaddr))
00929 elif type == "static":
00930 self.interfaces.append(StaticRoute(iface, opts, FIRST_IFACE_RULE + i, pingaddr))
00931 else:
00932 raise Exception("Unknown type for an interface: %s"%type)
00933
00934 self.interface_names = [ iface.name for iface in self.interfaces ]
00935
00936 def update(self):
00937
00938 for iface in self.interfaces:
00939 iface.update()
00940
00941
00942 self.diags = []
00943 self.diags.append(('Tunnel Interface', self.tunnel_interface))
00944 if self.active_iface >= 0:
00945 act_iface = self.interfaces[self.active_iface]
00946 self.diags.append(('Active Interface', act_iface.iface ))
00947 self.diags += act_iface.diags
00948 if act_iface.goodness > 95:
00949 self.diag_summary = "Active interface %s running strong"%act_iface.iface
00950 self.diag_level = 0
00951 elif act_iface.goodness > 50:
00952 self.diag_summary = "Active interface %s is lossy"%act_iface.iface
00953 self.diag_level = 1
00954 else:
00955 self.diag_summary = "Active interface %s is very poor"%act_iface.iface
00956 self.diag_level = 2
00957 else:
00958 self.diags.append(('Active Interface', "none"))
00959 self.diag_summary = 'No active interface'
00960 self.diag_level = 2
00961
00962 def prepare_diagnostics(self):
00963 pass
00964
00965 def shutdown(self):
00966
00967 if hasattr(self, "interfaces"):
00968 for iface in self.interfaces:
00969
00970 safe_shutdown(iface.shutdown)
00971 safe_shutdown(netlink_monitor.shutdown)
00972
00973
00974
00975 safe_shutdown(self.routing_rules.shutdown)
00976
00977 def make_active(self, iface):
00978 if not iface:
00979 self.active = -1
00980 return
00981 self.make_active_multiple([iface])
00982
00983 def _set_tunnel_rule(self, priority, rule):
00984 System("ip rule add priority %i %s"%(priority, rule))
00985 if priority in self._tunnel_rules:
00986 System("ip rule del priority %i %s"%(priority, self._tunnel_rules[priority]))
00987 self._tunnel_rules[priority] = rule
00988
00989 def make_active_non_tunnel(self, iface):
00990 if not iface:
00991 self.active = -1
00992 return
00993
00994 self.active_iface = self.interface_names.index(iface.name)
00995 self._set_tunnel_rule(TUNNEL_RULE,"table %i"%iface.tableid)
00996
00997 def make_active_multiple(self, iface):
00998 n = len(iface)
00999 if not n:
01000 self.active_iface = -1
01001 return
01002
01003 self.active_iface = self.interface_names.index(iface[0].name)
01004 for i in range(0, n):
01005 self._set_tunnel_rule(TUNNEL_RULE+i,"table %i to %s"%(iface[i].tableid, self.base_station))
01006
01007
01008
01009 class SelectionStrategy:
01010 def __init__(self, config):
01011 self.ns = NetworkSelector(config)
01012
01013 def update(self):
01014 ns = self.ns
01015 ns.update()
01016 print >> strategy_str
01017 print >> strategy_str, log_time_string(time.time())
01018 self.do_update()
01019 ns.prepare_diagnostics()
01020
01021 def shutdown(self):
01022 safe_shutdown(self.ns.shutdown)
01023
01024 class StatGatherer:
01025 def __init__(self):
01026 self.max = -1e1000
01027 self.min = 1e1000
01028 self.sum = 0.
01029 self.count = 0
01030 self.avg = 0.
01031 self.dev = 0.
01032 self.sqrsum = 0.
01033
01034 def append(self, value):
01035 self.max = max(self.max, value)
01036 self.min = min(self.min, value)
01037 self.sum += value
01038 self.sqrsum += value * value
01039 self.count += 1
01040 self.avg = self.sum / self.count
01041 self.dev = math.sqrt(self.sqrsum / self.count - self.avg * self.avg)
01042
01043 def to_string(self):
01044 return "avg: %5.1f dev: %5.1f min: %5.1s max: %5.1f count: %5i"%(self.avg, self.dev, self.min, self.max, self.count)
01045
01046 class LinkBringupTimeMeasurementStrategy(SelectionStrategy):
01047 def __init__(self, config):
01048 SelectionStrategy.__init__(self, config)
01049 self.start_times = len(self.ns.interfaces) * [time.time()]
01050 self.stats = [ StatGatherer() for iface in self.ns.interfaces]
01051
01052 def do_update(self):
01053 ns = self.ns
01054 now = time.time()
01055 for i in range(0, len(ns.interfaces)):
01056 iface = ns.interfaces[i]
01057 if iface.goodness >= 90:
01058 self.stats[i].append(now - self.start_times[i])
01059 iface.startover()
01060 self.start_times[i] = now
01061 print >> strategy_str, "%s %5.0f %5.1fs %4.1fs %s"%(iface.name, iface.goodness, now - self.start_times[i], iface.timeout_time - now, self.stats[i].to_string())
01062 print >> strategy_str
01063
01064 class LinkStabilityTimeMeasurementStrategy(SelectionStrategy):
01065 def __init__(self, config):
01066 SelectionStrategy.__init__(self, config)
01067 self.is_up = len(self.ns.interfaces) * [ False ]
01068 self.start_times = len(self.ns.interfaces) * [time.time()]
01069 self.start_stats = [ StatGatherer() for iface in self.ns.interfaces]
01070 self.longevity_stats = [ StatGatherer() for iface in self.ns.interfaces ]
01071
01072 def do_update(self):
01073 ns = self.ns
01074 now = time.time()
01075 for i in range(0, len(ns.interfaces)):
01076 iface = ns.interfaces[i]
01077 is_now_up = self.is_up[i]
01078 if iface.goodness >= 90:
01079 is_now_up = True
01080 elif iface.goodness < 0:
01081 is_now_up = False
01082 if is_now_up:
01083 if iface.goodness >= 90:
01084 iface.increase_timeout()
01085 else:
01086 iface.decrease_timeout(5)
01087 if self.is_up[i] != is_now_up:
01088 (self.start_stats if is_now_up else self.longevity_stats)[i].append(now - self.start_times[i])
01089 self.start_times[i] = now
01090 self.is_up[i] = is_now_up
01091
01092
01093 print >> strategy_str, iface.name, "%5.0f"%iface.goodness, "%5.1fs"%(now - self.start_times[i]), "%4.1fs"%(iface.timeout_time - now), "START", self.start_stats[i].to_string(), "LONGEVITY", self.longevity_stats[i].to_string()
01094
01095 class SimpleSelectionStrategy(SelectionStrategy):
01096 def __init__(self, config):
01097 self.best_wireless = -1
01098 SelectionStrategy.__init__(self, config)
01099 try:
01100 self.reliability_thresh = config['reliability_threshold']
01101 except:
01102 self.reliability_thresh = 90
01103
01104 def do_update(self):
01105 ns = self.ns
01106
01107 iface_with_sort_param = []
01108 best_bssid = ns.interfaces[self.best_wireless].bssid if self.best_wireless != -1 else None
01109 for i in ns.interfaces:
01110 if i.goodness <= 0:
01111 continue
01112 bonus = 0
01113 if self.best_wireless != -1:
01114 if i == ns.interfaces[self.best_wireless]:
01115 bonus += 5
01116 elif i.bssid != best_bssid:
01117 bonus -= 5
01118
01119
01120 sort_param = i.goodness + i.reliability + i.priority + bonus
01121 iface_with_sort_param.append((i, sort_param))
01122 if iface_with_sort_param:
01123 iface_with_sort_param.sort(key = lambda tuple: tuple[1], reverse = True)
01124 iface_sorted, _ = zip(*iface_with_sort_param)
01125 else:
01126 iface_sorted = []
01127
01128 ns.make_active_multiple(iface_sorted)
01129
01130
01131 iface_types = map(lambda x: x.__class__, iface_sorted)
01132 try:
01133 wireless_index = iface_types.index(WirelessInterface)
01134 self.best_wireless = ns.interfaces.index(iface_sorted[wireless_index])
01135 except:
01136 self.best_wireless = -1
01137
01138
01139 for i in range(0,len(ns.interfaces)):
01140 interface = ns.interfaces[i]
01141 if interface.goodness < 0:
01142 pass
01143 elif i == self.best_wireless:
01144 if interface.goodness > 0:
01145
01146 interface.increase_timeout()
01147 elif interface.reliability < self.reliability_thresh or interface.goodness < 50:
01148
01149 interface.decrease_timeout(3)
01150 else:
01151
01152 interface.increase_timeout()
01153
01154
01155 for iface in ns.interfaces:
01156 try:
01157 rank = iface_sorted.index(iface) + 1
01158 if rank == 1:
01159 is_active = "active"
01160 else:
01161 is_active = "#%i"%rank
01162 if self.best_wireless != -1 and iface == ns.interfaces[self.best_wireless]:
01163 is_active += ", best wifi"
01164 except ValueError:
01165 is_active = ""
01166 print >> strategy_str, "%10s %5.1f %17s %7.3f %3.0f %s"%(iface.name, (iface.timeout_time - time.time()), iface.bssid, iface.goodness, iface.reliability, is_active)
01167
01168 class AlwaysSwitchSelectionStrategy(SelectionStrategy):
01169 def __init__(self, config):
01170 SelectionStrategy.__init__(self, config)
01171
01172 def do_update(self):
01173 ns = self.ns
01174
01175 goodnesses = [ i.goodness for i in ns.interfaces ]
01176 reliabilities = [ i.reliability for i in ns.interfaces ]
01177 priorities = [ i.priority for i in ns.interfaces ]
01178
01179
01180 sort_param = [ sum(tuple) for tuple in zip(goodnesses, reliabilities, priorities) ]
01181 next_iface = (ns.active_iface + 1) % len(sort_param)
01182 if sort_param[next_iface] > 180:
01183 best_iface = next_iface
01184 else:
01185 best_sort_param = max(sort_param)
01186 best_iface = sort_param.index(best_sort_param)
01187 if goodnesses[best_iface] < 0:
01188 ns.active_iface = -1
01189 elif best_iface != ns.active_iface:
01190 ns.active_iface = best_iface
01191 ns.make_active(ns.interfaces[ns.active_iface])
01192
01193
01194 for i in range(0,len(ns.interfaces)):
01195 interface = ns.interfaces[i]
01196 if interface.goodness < 0:
01197 pass
01198 elif i == ns.active_iface:
01199 if interface.goodness > 0:
01200
01201 interface.increase_timeout()
01202 elif interface.reliability < 90 or interface.goodness < 50:
01203
01204 interface.decrease_timeout(3)
01205 else:
01206
01207 interface.increase_timeout()
01208
01209
01210 for iface in ns.interfaces:
01211 if ns.active_iface != -1 and iface == ns.interfaces[ns.active_iface]:
01212 is_active = "active"
01213 else:
01214 is_active = ""
01215 print >> strategy_str, "%s %.1f %s %.3f %.0f %s"%(iface.name, (iface.timeout_time - time.time()), iface.bssid, iface.goodness, iface.reliability, is_active)
01216
01217 def main(config_file, strategy, supervisor_function = None):
01218 if os.getuid() != 0:
01219 print >> console, "roam.py must be run as root!"
01220 sys.exit(1)
01221 with open(config_file, 'r') as f:
01222 config = yaml.load(f.read())
01223 try:
01224 try:
01225 try:
01226 s = strategy(config)
01227
01228 while True:
01229 s.update()
01230 if supervisor_function:
01231 try:
01232 supervisor_function(s)
01233 except:
01234 print "Exception in supervisor_function."
01235 traceback.print_exc(10)
01236 print
01237 time.sleep(1)
01238
01239 except KeyboardInterrupt:
01240 print "Exiting on CTRL-C"
01241 except:
01242 traceback.print_exc(10)
01243 print
01244 raise
01245 finally:
01246 try:
01247 safe_shutdown(s.shutdown)
01248 except:
01249 traceback.print_exc(10)
01250 print
01251
01252 print
01253 print "End of main reached. Waiting for threads to shut down."
01254 time.sleep(0.1)
01255 while True:
01256 threads = threading.enumerate()
01257 non_daemon = sum(0 if t.daemon else 1 for t in threads)
01258 if non_daemon == 1:
01259 break
01260 print
01261 print "Remaining threads:", non_daemon, len(threads)
01262 for t in threads:
01263 print ("daemon: " if t.daemon else "regular:"), t.name
01264 time.sleep(1)
01265
01266 if __name__ == "__main__":
01267 main('roam_config.yaml', SimpleSelectionStrategy)
01268
01269
01270