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 BLOCK_TUNNEL_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
00557 self.dhclient = DhcpClient(iface, self.bound, self.deconfigured, self.leasefail)
00558 self.startover()
00559
00560 def update1(self):
00561 time_to_timeout = self.timeout_time - time.time()
00562 if time_to_timeout < 0:
00563 print "Interface", self.name, "timed out."
00564 self.startover()
00565
00566 NetworkConnection.update1(self)
00567
00568 self.diags.append(("Time to interface timeout (s)", time_to_timeout))
00569
00570 def linkchange(self, up):
00571
00572 self.set_timeout(self.start_over_timeout)
00573
00574 if up:
00575 if self.status == NOLINK:
00576 self.bssid = "Wired"
00577 self.status = NOIP
00578 self.set_timeout(self.dhcp_timeout)
00579 self.dhclient.renew()
00580 else:
00581 if self.status != NOLINK:
00582 self.bssid = "NoLink"
00583 self.status = NOLINK
00584 self.udp_monitor.stop_monitor()
00585 self.dhclient.release()
00586 pass
00587
00588 def set_routes(self, addr, netbits):
00589 if not self.is_bound:
00590 return
00591 flushiprule(self.tableid)
00592
00593 System("ip rule add priority %i from %s table %i"%(self.tableid,addr,self.tableid))
00594 System("ip route flush table %i"%self.tableid)
00595 System("ip route add table %i default dev %s via %s src %s onlink"%(self.tableid, self.iface, self.gateway, addr))
00596 net = ipaddr.IPv4Network("%s/%s"%(addr,netbits))
00597
00598 print("ip route add table %i %s/%s dev %s src %s"%(self.tableid, net.network, net.prefixlen, self.iface, addr))
00599 System("ip route add table %i %s/%s dev %s src %s"%(self.tableid, net.network, net.prefixlen, self.iface, addr))
00600
00601 def bound(self, iface, ip, netmask, gateway):
00602 print "Bound!", self.status
00603
00604
00605 self.increase_timeout(self.start_over_timeout)
00606 self.is_bound = True
00607 self.ip = ip
00608 self.netmask = netmask
00609 self.gateway = gateway
00610
00611
00612
00613 System("ifconfig %s 0.0.0.0"%iface)
00614 net = ipaddr.IPv4Network("%s/%s"%(ip, netmask))
00615 System("ip addr add %s/%s dev %s"%(ip, net.prefixlen, iface))
00616
00617 System("arping -q -c 1 -A -I %s %s"%(iface,ip))
00618 ConfigureRpFilter(iface)
00619 print "Bound done!", self.status
00620
00621 def startover(self):
00622 flushiprule(self.tableid)
00623 System("ifconfig %s 0.0.0.0 down"%self.iface)
00624 System("ifconfig %s up"%self.iface)
00625 self.set_timeout(self.start_over_timeout)
00626
00627 def leasefail(self, iface):
00628 if self.status > NOLINK:
00629 self.startover()
00630
00631 def deconfigured(self, iface):
00632 if self.is_bound:
00633 self.is_bound = False
00634 flushiprule(self.tableid)
00635 System("ifconfig %s 0.0.0.0 down"%iface)
00636 System("ifconfig %s up"%iface)
00637
00638 def shutdown(self):
00639 NetworkConnection.shutdown(self)
00640 System("ifconfig %s down"%self.iface)
00641 safe_shutdown(self.dhclient.shutdown)
00642
00643 def decrease_timeout(self, delay):
00644
00645 self.timeout_time = min(time.time() + delay, self.timeout_time)
00646
00647 def increase_timeout(self, delay = 1e1000):
00648
00649 self.timeout_time = max(time.time() + delay, self.timeout_time)
00650
00651 def set_timeout(self, delay):
00652
00653 self.timeout_time = time.time() + delay
00654
00655 class WirelessInterface(DhcpInterface):
00656 def __init__(self, iface, config, tableid, pingtarget):
00657 self.startover_count = 0
00658
00659 DhcpInterface.__init__(self, iface, config, tableid, pingtarget)
00660
00661 self.wifi = pythonwifi.iwlibs.Wireless(iface)
00662 self.iwinfo = pythonwifi.iwlibs.WirelessInfo(iface)
00663
00664 self.supplicant = WpaSupplicant(iface, config['wpa_config'])
00665 self.initialized = True
00666 netlink_monitor.register_link(iface, self.linkchange)
00667
00668 def linkchange(self, up):
00669 if not self.initialized:
00670 return
00671
00672 if up:
00673 self.startover_count = 0
00674
00675
00676
00677 DhcpInterface.linkchange(self, up)
00678
00679 def startover(self):
00680 if self.startover_count > 1:
00681 self.startover_count = 0
00682 self.supplicant.restart()
00683 else:
00684 self.startover_count += 1
00685
00686 DhcpInterface.startover(self)
00687
00688 def update(self):
00689 self.update1()
00690
00691 try:
00692 self.essid = self.wifi.getEssid()
00693 self.diags.append(('ESSID', self.essid))
00694 except Exception, e:
00695 if self.status != NOLINK:
00696 traceback.print_exc(10)
00697 print
00698 self.diags.append(('ESSID', 'Error collecting data.'))
00699 self.essid = "###ERROR-COLLECTING-DATA###"
00700
00701 try:
00702 self.bssid = self.wifi.getAPaddr()
00703 self.diags.append(('BSSID', self.bssid))
00704 except Exception, e:
00705 if self.status != NOLINK:
00706 traceback.print_exc(10)
00707 print
00708 self.bssid = "00:00:00:00:00:00"
00709 self.diags.append(('BSSID', 'Error collecting data.'))
00710
00711 try:
00712 self.wifi_txpower = 10**(self.wifi.wireless_info.getTXPower().value/10.)
00713 self.diags.append(('TX Power (mW)', self.wifi_txpower))
00714 self.wifi_txpower = "%.1f mW"%self.wifi_txpower
00715 except Exception, e:
00716 if str(e).find("Operation not supported") == -1 and self.status != NOLINK:
00717 traceback.print_exc(10)
00718 print
00719 self.diags.append(('TX Power (mW)', 'Error collecting data.'))
00720 self.wifi_txpower = "unknown"
00721
00722 try:
00723 self.wifi_frequency = self.wifi.wireless_info.getFrequency().getFrequency()
00724 self.diags.append(('Frequency (Gz)', "%.4f"%(self.wifi_frequency/1e9)))
00725 except Exception, e:
00726 if self.status != NOLINK:
00727 traceback.print_exc(10)
00728 print
00729 self.wifi_frequency = 0
00730 self.diags.append(('Frequency', 'Error collecting data.'))
00731
00732 got_stats = False
00733 if self.status != NOLINK:
00734 try:
00735 stat, qual, discard, missed_beacon = self.wifi.getStatistics()
00736 max_quality = self.wifi.getQualityMax().quality
00737 quality = qual.quality * 100 / max_quality
00738 self.diags.append(('Quality', quality))
00739 self.diags.append(('Signal (dB)', qual.siglevel))
00740 self.diags.append(('Noise (dB)', qual.nlevel))
00741 self.diags.append(('SNR (dB)', qual.siglevel - qual.nlevel))
00742 self.wifi_signal = qual.siglevel
00743 self.wifi_noise = qual.nlevel
00744 self.wifi_quality = quality
00745 self.reliability = quality
00746 got_stats = True
00747 except Exception, e:
00748 print "Error getting wireless stats on interface %s: %s"%(self.iface, str(e))
00749
00750 if not got_stats:
00751
00752 print
00753 self.reliability = 0
00754 self.wifi_quality = -1
00755 self.wifi_noise = 1e1000
00756 self.wifi_signal = -1e1000
00757 for s in [ 'Quality', 'Signal', 'Noise' ]:
00758 if self.status != NOLINK:
00759 self.diags.append((s, 'Error collecting data.'))
00760 else:
00761 self.diags.append((s, 'Unknown'))
00762
00763 self.wifi_rate = None
00764 if self.status != NOLINK:
00765 try:
00766 self.wifi_rate = self.wifi.wireless_info.getBitrate().value
00767 except:
00768 pass
00769 if self.wifi_rate is None:
00770 try:
00771 self.wifi_rate = self.iwinfo.getBitrate().value
00772 except:
00773 pass
00774 if self.wifi_rate is not None:
00775 self.diags.append(('TX Rate (Mbps)', self.wifi_rate / 1e6))
00776 self.wifi_rate = self.wifi._formatBitrate(self.wifi_rate)
00777 else:
00778 if self.status != NOLINK:
00779 print "Unable to determine TX rate on interface", self.iface
00780 self.diags.append(('TX Rate (Mbps)', 'Error collecting data.'))
00781 else:
00782 self.diags.append(('TX Rate (Mbps)', 'Unknown'))
00783 self.wifi_rate = "Unknown"
00784
00785 self.update2()
00786
00787 def shutdown(self):
00788 DhcpInterface.shutdown(self)
00789 safe_shutdown(self.supplicant.shutdown)
00790
00791 class WiredInterface(DhcpInterface):
00792 def __init__(self, iface, config, tableid, pingtarget):
00793 self.initialized = False
00794 DhcpInterface.__init__(self, iface, config, tableid, pingtarget)
00795 self.initialized = True
00796 netlink_monitor.register_link(iface, self.linkchange)
00797
00798 def linkchange(self, up):
00799 if up != (self.status != NOLINK) and self.initialized:
00800 os.system('beep')
00801 DhcpInterface.linkchange(self, up)
00802
00803 def update(self):
00804 self.update1()
00805 self.reliability = 100
00806 self.update2()
00807
00808 def shutdown(self):
00809 self.initialized = False
00810 DhcpInterface.shutdown(self)
00811
00812 class PacketMarker:
00813 def __init__(self, basestation):
00814 print "Initializing PacketMarker."
00815 self.basestation = basestation
00816 self.rules = []
00817
00818 self.rules.append("OUTPUT -t mangle -d %s -p udp --dport 1194 -j MARK --set-mark 1"%basestation)
00819
00820 self.rules.append("OUTPUT -t mangle -d %s -p udp --dport 6868 -j MARK --set-mark 2"%basestation)
00821 self.playrules("-D", True)
00822 self.playrules("-A", False)
00823 print "PacketMarker initialized."
00824
00825 def playrules(self, command, repeat):
00826 for rule in self.rules:
00827 while True:
00828 if System("iptables %s %s"%(command, rule)).errcode:
00829 break
00830 if not repeat:
00831 break
00832
00833 def shutdown(self):
00834 self.playrules("-D", False)
00835
00836 class RoutingRules:
00837 def __init__(self, numinterfaces, localnets, tuniface):
00838 self.numinterfaces = numinterfaces
00839 self.tuniface = tuniface
00840 self.flushall()
00841
00842
00843
00844
00845
00846
00847 for net in localnets:
00848 System("ip rule add priority %i to %s lookup main"%(LOCAL_RULE, net))
00849 System("ip rule add priority %i lookup %i"%(DEFAULT_RULE,DEFAULT_RULE))
00850
00851
00852 netlink_monitor.register_link(self.tuniface, self.refresh)
00853
00854 def refresh(self, up):
00855 if up:
00856 System("ip route replace table %i default dev %s"%(DEFAULT_RULE, self.tuniface))
00857
00858 def flushall(self):
00859
00860 for i in range(0,self.numinterfaces):
00861 flushiprule(FIRST_IFACE_RULE + i)
00862 flushiprule(TUNNEL_RULE + i)
00863 flushiprule(DEFAULT_RULE)
00864 flushiprule(LOCAL_RULE)
00865
00866
00867
00868
00869
00870 for i in range(0,self.numinterfaces):
00871 self.flushtable(FIRST_IFACE_RULE + i)
00872 self.flushtable(DEFAULT_RULE)
00873
00874
00875 def flushtable(self, tab_id):
00876 System("ip route flush table %i"%tab_id)
00877
00878 def shutdown(self):
00879 self.flushall()
00880
00881 class ConfigureArp:
00882 def __init__(self):
00883 System("sysctl net.ipv4.conf.all.arp_filter=1")
00884
00885 class ConfigureRpFilter:
00886 def __init__(self, iface):
00887 System("sysctl net.ipv4.conf.%s.rp_filter=0"%iface)
00888
00889 class KillServices:
00890 def __init__(self):
00891 System("killall wpa_supplicant udhcpc 2> /dev/null")
00892
00893 class NetworkSelector:
00894 def __init__(self, config):
00895 interfaces_config = config['interfaces']
00896 self.base_station = socket.gethostbyname(config['base_station'])
00897 pingaddr = (self.base_station, int(config['ping_port']))
00898 self.tunnel_interface = config['tunnel_interface']
00899 local_networks = config['local_networks']
00900
00901 KillServices()
00902 global netlink_monitor
00903 netlink_monitor = NetlinkMonitor()
00904 self.pkt_marker = PacketMarker(self.base_station)
00905 ConfigureArp()
00906 ConfigureRpFilter('all')
00907 self.routing_rules = RoutingRules(len(interfaces_config), local_networks, self.tunnel_interface)
00908 self.interfaces = []
00909 self.active_iface = -1
00910 self._tunnel_rules = {}
00911 self._set_tunnel_rule(TUNNEL_RULE, "blackhole")
00912
00913 i = -1
00914 for iface in interfaces_config:
00915 i += 1
00916 opts = interfaces_config[iface]
00917 type = opts['type']
00918 if type == "wireless":
00919 if 'wpa_config' not in opts and 'wpa_config' in config:
00920 opts['wpa_config'] = config['wpa_config']
00921 self.interfaces.append(WirelessInterface(iface, opts, FIRST_IFACE_RULE + i, pingaddr))
00922 elif type == "wired":
00923 self.interfaces.append(WiredInterface(iface, opts, FIRST_IFACE_RULE + i, pingaddr))
00924 elif type == "static":
00925 self.interfaces.append(StaticRoute(iface, opts, FIRST_IFACE_RULE + i, pingaddr))
00926 else:
00927 raise Exception("Unknown type for an interface: %s"%type)
00928
00929 self.interface_names = [ iface.name for iface in self.interfaces ]
00930
00931 def update(self):
00932
00933 for iface in self.interfaces:
00934 iface.update()
00935
00936
00937 self.diags = []
00938 self.diags.append(('Tunnel Interface', self.tunnel_interface))
00939 if self.active_iface >= 0:
00940 act_iface = self.interfaces[self.active_iface]
00941 self.diags.append(('Active Interface', act_iface.iface ))
00942 self.diags += act_iface.diags
00943 if act_iface.goodness > 95:
00944 self.diag_summary = "Active interface %s running strong"%act_iface.iface
00945 self.diag_level = 0
00946 elif act_iface.goodness > 50:
00947 self.diag_summary = "Active interface %s is lossy"%act_iface.iface
00948 self.diag_level = 1
00949 else:
00950 self.diag_summary = "Active interface %s is very poor"%act_iface.iface
00951 self.diag_level = 2
00952 else:
00953 self.diags.append(('Active Interface', "none"))
00954 self.diag_summary = 'No active interface'
00955 self.diag_level = 2
00956
00957 def prepare_diagnostics(self):
00958 pass
00959
00960 def shutdown(self):
00961
00962 if hasattr(self, "interfaces"):
00963 for iface in self.interfaces:
00964
00965 safe_shutdown(iface.shutdown)
00966 safe_shutdown(netlink_monitor.shutdown)
00967
00968 safe_shutdown(self.pkt_marker.shutdown)
00969
00970 safe_shutdown(self.routing_rules.shutdown)
00971
00972 def make_active(self, iface):
00973 if not iface:
00974 self.active = -1
00975 return
00976 self.make_active_multiple([iface])
00977
00978 def _set_tunnel_rule(self, priority, rule):
00979 System("ip rule add priority %i %s"%(priority, rule))
00980 if priority in self._tunnel_rules:
00981 System("ip rule del priority %i %s"%(priority, self._tunnel_rules[priority]))
00982 self._tunnel_rules[priority] = rule
00983
00984 def make_active_non_tunnel(self, iface):
00985 if not iface:
00986 self.active = -1
00987 return
00988
00989 self.active_iface = self.interface_names.index(iface.name)
00990 self._set_tunnel_rule(TUNNEL_RULE,"table %i"%iface.tableid)
00991
00992 def make_active_multiple(self, iface):
00993 n = len(iface)
00994 if not n:
00995 self.active_iface = -1
00996 return
00997
00998 self.active_iface = self.interface_names.index(iface[0].name)
00999 for i in range(0, n):
01000 self._set_tunnel_rule(TUNNEL_RULE+i,"table %i to %s"%(iface[i].tableid, self.base_station))
01001
01002
01003
01004 class SelectionStrategy:
01005 def __init__(self, config):
01006 self.ns = NetworkSelector(config)
01007
01008 def update(self):
01009 ns = self.ns
01010 ns.update()
01011 print >> strategy_str
01012 print >> strategy_str, log_time_string(time.time())
01013 self.do_update()
01014 ns.prepare_diagnostics()
01015
01016 def shutdown(self):
01017 safe_shutdown(self.ns.shutdown)
01018
01019 class StatGatherer:
01020 def __init__(self):
01021 self.max = -1e1000
01022 self.min = 1e1000
01023 self.sum = 0.
01024 self.count = 0
01025 self.avg = 0.
01026 self.dev = 0.
01027 self.sqrsum = 0.
01028
01029 def append(self, value):
01030 self.max = max(self.max, value)
01031 self.min = min(self.min, value)
01032 self.sum += value
01033 self.sqrsum += value * value
01034 self.count += 1
01035 self.avg = self.sum / self.count
01036 self.dev = math.sqrt(self.sqrsum / self.count - self.avg * self.avg)
01037
01038 def to_string(self):
01039 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)
01040
01041 class LinkBringupTimeMeasurementStrategy(SelectionStrategy):
01042 def __init__(self, config):
01043 SelectionStrategy.__init__(self, config)
01044 self.start_times = len(self.ns.interfaces) * [time.time()]
01045 self.stats = [ StatGatherer() for iface in self.ns.interfaces]
01046
01047 def do_update(self):
01048 ns = self.ns
01049 now = time.time()
01050 for i in range(0, len(ns.interfaces)):
01051 iface = ns.interfaces[i]
01052 if iface.goodness >= 90:
01053 self.stats[i].append(now - self.start_times[i])
01054 iface.startover()
01055 self.start_times[i] = now
01056 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())
01057 print >> strategy_str
01058
01059 class LinkStabilityTimeMeasurementStrategy(SelectionStrategy):
01060 def __init__(self, config):
01061 SelectionStrategy.__init__(self, config)
01062 self.is_up = len(self.ns.interfaces) * [ False ]
01063 self.start_times = len(self.ns.interfaces) * [time.time()]
01064 self.start_stats = [ StatGatherer() for iface in self.ns.interfaces]
01065 self.longevity_stats = [ StatGatherer() for iface in self.ns.interfaces ]
01066
01067 def do_update(self):
01068 ns = self.ns
01069 now = time.time()
01070 for i in range(0, len(ns.interfaces)):
01071 iface = ns.interfaces[i]
01072 is_now_up = self.is_up[i]
01073 if iface.goodness >= 90:
01074 is_now_up = True
01075 elif iface.goodness < 0:
01076 is_now_up = False
01077 if is_now_up:
01078 if iface.goodness >= 90:
01079 iface.increase_timeout()
01080 else:
01081 iface.decrease_timeout(5)
01082 if self.is_up[i] != is_now_up:
01083 (self.start_stats if is_now_up else self.longevity_stats)[i].append(now - self.start_times[i])
01084 self.start_times[i] = now
01085 self.is_up[i] = is_now_up
01086
01087
01088 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()
01089
01090 class SimpleSelectionStrategy(SelectionStrategy):
01091 def __init__(self, config):
01092 self.best_wireless = -1
01093 SelectionStrategy.__init__(self, config)
01094 try:
01095 self.reliability_thresh = config['reliability_threshold']
01096 except:
01097 self.reliability_thresh = 90
01098
01099 def do_update(self):
01100 ns = self.ns
01101
01102 iface_with_sort_param = []
01103 best_bssid = ns.interfaces[self.best_wireless].bssid if self.best_wireless != -1 else None
01104 for i in ns.interfaces:
01105 if i.goodness <= 0:
01106 continue
01107 bonus = 0
01108 if self.best_wireless != -1:
01109 if i == ns.interfaces[self.best_wireless]:
01110 bonus += 5
01111 elif i.bssid != best_bssid:
01112 bonus -= 5
01113
01114
01115 sort_param = i.goodness + i.reliability + i.priority + bonus
01116 iface_with_sort_param.append((i, sort_param))
01117 if iface_with_sort_param:
01118 iface_with_sort_param.sort(key = lambda tuple: tuple[1], reverse = True)
01119 iface_sorted, _ = zip(*iface_with_sort_param)
01120 else:
01121 iface_sorted = []
01122
01123 ns.make_active_multiple(iface_sorted)
01124
01125
01126 iface_types = map(lambda x: x.__class__, iface_sorted)
01127 try:
01128 wireless_index = iface_types.index(WirelessInterface)
01129 self.best_wireless = ns.interfaces.index(iface_sorted[wireless_index])
01130 except:
01131 self.best_wireless = -1
01132
01133
01134 for i in range(0,len(ns.interfaces)):
01135 interface = ns.interfaces[i]
01136 if interface.goodness < 0:
01137 pass
01138 elif i == self.best_wireless:
01139 if interface.goodness > 0:
01140
01141 interface.increase_timeout()
01142 elif interface.reliability < self.reliability_thresh or interface.goodness < 50:
01143
01144 interface.decrease_timeout(3)
01145 else:
01146
01147 interface.increase_timeout()
01148
01149
01150 for iface in ns.interfaces:
01151 try:
01152 rank = iface_sorted.index(iface) + 1
01153 if rank == 1:
01154 is_active = "active"
01155 else:
01156 is_active = "#%i"%rank
01157 if self.best_wireless != -1 and iface == ns.interfaces[self.best_wireless]:
01158 is_active += ", best wifi"
01159 except ValueError:
01160 is_active = ""
01161 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)
01162
01163 class AlwaysSwitchSelectionStrategy(SelectionStrategy):
01164 def __init__(self, config):
01165 SelectionStrategy.__init__(self, config)
01166
01167 def do_update(self):
01168 ns = self.ns
01169
01170 goodnesses = [ i.goodness for i in ns.interfaces ]
01171 reliabilities = [ i.reliability for i in ns.interfaces ]
01172 priorities = [ i.priority for i in ns.interfaces ]
01173
01174
01175 sort_param = [ sum(tuple) for tuple in zip(goodnesses, reliabilities, priorities) ]
01176 next_iface = (ns.active_iface + 1) % len(sort_param)
01177 if sort_param[next_iface] > 180:
01178 best_iface = next_iface
01179 else:
01180 best_sort_param = max(sort_param)
01181 best_iface = sort_param.index(best_sort_param)
01182 if goodnesses[best_iface] < 0:
01183 ns.active_iface = -1
01184 elif best_iface != ns.active_iface:
01185 ns.active_iface = best_iface
01186 ns.make_active(ns.interfaces[ns.active_iface])
01187
01188
01189 for i in range(0,len(ns.interfaces)):
01190 interface = ns.interfaces[i]
01191 if interface.goodness < 0:
01192 pass
01193 elif i == ns.active_iface:
01194 if interface.goodness > 0:
01195
01196 interface.increase_timeout()
01197 elif interface.reliability < 90 or interface.goodness < 50:
01198
01199 interface.decrease_timeout(3)
01200 else:
01201
01202 interface.increase_timeout()
01203
01204
01205 for iface in ns.interfaces:
01206 if ns.active_iface != -1 and iface == ns.interfaces[ns.active_iface]:
01207 is_active = "active"
01208 else:
01209 is_active = ""
01210 print >> strategy_str, "%s %.1f %s %.3f %.0f %s"%(iface.name, (iface.timeout_time - time.time()), iface.bssid, iface.goodness, iface.reliability, is_active)
01211
01212 def main(config_file, strategy, supervisor_function = None):
01213 if os.getuid() != 0:
01214 print >> console, "roam.py must be run as root!"
01215 sys.exit(1)
01216 with open(config_file, 'r') as f:
01217 config = yaml.load(f.read())
01218 try:
01219 try:
01220 try:
01221 s = strategy(config)
01222
01223 while True:
01224 s.update()
01225 if supervisor_function:
01226 try:
01227 supervisor_function(s)
01228 except:
01229 print "Exception in supervisor_function."
01230 traceback.print_exc(10)
01231 print
01232 time.sleep(1)
01233
01234 except KeyboardInterrupt:
01235 print "Exiting on CTRL-C"
01236 except:
01237 traceback.print_exc(10)
01238 print
01239 raise
01240 finally:
01241 try:
01242 safe_shutdown(s.shutdown)
01243 except:
01244 traceback.print_exc(10)
01245 print
01246
01247 print
01248 print "End of main reached. Waiting for threads to shut down."
01249 time.sleep(0.1)
01250 while True:
01251 threads = threading.enumerate()
01252 non_daemon = sum(0 if t.daemon else 1 for t in threads)
01253 if non_daemon == 1:
01254 break
01255 print
01256 print "Remaining threads:", non_daemon, len(threads)
01257 for t in threads:
01258 print ("daemon: " if t.daemon else "regular:"), t.name
01259 time.sleep(1)
01260
01261 if __name__ == "__main__":
01262 main('roam_config.yaml', SimpleSelectionStrategy)
01263
01264
01265