#!/usr/bin/env python ''' # Convert traceroute output into directed graphviz graph, mostly usefull for # exploring routing paths and stabilities. # # Example: ./tviz.py `~/wlhotlist/genesis/tools/gformat.py list up systems` | dot -Tpng | display # # Rick van der Zwet # ''' import subprocess import sys import yaml import hashlib import logging import argparse from collections import defaultdict probe_count = 10 logging.basicConfig(level=logging.DEBUG, stream=sys.stderr) logger = logging.getLogger() def get_data(hostname): # Get output p = subprocess.Popen(['traceroute', '-F', '-w', '1', '-q', str(probe_count), hostname, '1500'], stderr=subprocess.PIPE, stdout=subprocess.PIPE) (stdout, stderr) = p.communicate() if not p.returncode == 0: logger.error(stderr) return None return stdout.strip().split('\n') class RoutingError(Exception): pass def avg(x): x = filter(lambda x: x != None, x) if len(x) == 0: return None else: return sum(x) / len(x) def parse_result(lines): old_times = [0] * probe_count parent_ips = ['127.0.0.1'] route_path = [] avg_times = [] # Poor man traceroute parsing header = lines.pop(0) for line in lines: new_parent_ips = [] items = line.split() host = '' hop = '' ip = '' new_times = [] # State table parser state = 'hop_count' item = items.pop(0) while True: if state == 'hop_count': hop = item state = 'host' elif state == 'host': if not item == '*': host = item state = 'ip' else: new_times.append(None) elif state == 'ip': if item[0] != '(' or item[-1] != ')': raise ValueError("In line '%s'; Expected IP found '%s'" % (line, item)) ip = item[1:-1] # Detect routing loop if ip in route_path: raise RoutingError("loop detected with %s in %s" % (ip, route_path)) else: route_path.append(ip) # Create mapping ip2host[ip] = host # Prepare new parents if not item in new_parent_ips: new_parent_ips.append(ip) state = 'time' elif state == 'time': if len(item.split('.')) != 2: raise ValueError("In line '%s'; Expected time found '%s'" % (line, item)) state = 'time_suffix' new_times.append(float(item)) elif state == 'time_suffix': if item != 'ms': raise ValueError("In line '%s'; Expected time_suffix found '%s'" % (line, item)) # Remove dead values while(len(items) > 0 and ((items[0] == '*') or (items[0][0] == '!'))): new_times.append(None) items.pop(0) if len(items) == 0: # Add time (only single parent) for parent_ip in parent_ips: old_time = avg(old_times) new_time = avg(new_times) if old_time != None: time_diff = new_time - old_time route[(parent_ip,ip)].append(time_diff) else: route[(parent_ip,ip)].append(None) if len(items) > 1 and items[1] == 'ms': state = 'time' else: state = 'host' else: assert False, "Unknown state '%s'" % state if not items: # Update parent hosts if not new_parent_ips: bogus_ip = '0.0.0.%s' % hop for parent_ip in parent_ips: route[(parent_ip, bogus_ip)].append(9999) ip2host[bogus_ip] = 'unknown-at-hop-%s' % hop parent_ips = ['0.0.0.%s' % hop] else: avg_times.append(avg(new_times)) old_times = new_times assert len(old_times) == probe_count, "The probe count %s does not match (%s)!" % (probe_count, len(old_times)) parent_ips = new_parent_ips # Get to the next line break item = items.pop(0) route_hash = hashlib.md5(' '.join(route_path)).hexdigest() routes[route_hash] = route_path routes_avg_times[route_hash].append(avg_times) class makelabel(): def __init__(self): self.nid = 0 self.i2n = {} def make_label(self,ip): if not self.i2n.has_key(ip): node = 'n%i' % self.nid self.i2n[ip] = node self.nid += 1 def __getitem__(self,key): return self.i2n[key] def __str__(self): output = '' for ip,node in self.i2n.iteritems(): output += ' %s [label="%s\\n(%s)"];\n' % (node, ip2host[ip], ip) return output def make_result(args): output = '''\ digraph G { overlap = prism; node [fontsize="8", fontname="Ubuntu Mono"]; edge [fontsize="8", fontname="Ubuntu Mono"]; ''' ml = MakeLabel() # We cannot name our nodes using IP labels, syntax not supported if args.single_result: # Single result printing for (parent,child) in route.keys(): ml.make_label(parent) ml.make_label(child) output += str(ml) + '\n' for (parent,child),values in route.iteritems(): values = filter(lambda x: x != None, values) avg_values = avg(values) if avg_values == None: avg_values = [0] values = [0] output += ' %s -> %s [label="%.3f/%.3f/%.3f ms"];\n' % (ml[parent], ml[child],min(values), avg_values, max(values)) else: link = defaultdict(list) for route_hash,route_path in routes.iteritems(): avg_times_list = zip(*routes_avg_times[route_hash]) for ip in route_path: ml.make_label(ip) prev = route_path[0] for curr in route_path[1:]: avg_times = avg_times_list.pop(0) link[(prev, curr)].extend(avg_times) prev = curr output += str(ml) + '\n' for (prev,curr),avg_times in link.iteritems(): output += ' %s -> %s [label="%.3f/%.3f/%.3f ms"];\n' % ( ml[prev], ml[curr],min(avg_times), avg(avg_times), max(avg_times)) # All done output += "}" return output if __name__ == '__main__': parser = argparse.ArgumentParser(description=__doc__,formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument('hostnames', type=str, nargs='+', metavar='host') parser.add_argument('-s', '--single_result', default=False, action='store_true') args = parser.parse_args() try: if args.single_result: raise IOError, "Single Result only" data_store = yaml.load(open('traceroute.yaml')) route = data_store['route'] routes = data_store['routes'] routes_avg_times = data_store['routes_avg_times'] ip2host = data_store['ip2host'] except IOError: route = defaultdict(list) routes = dict() routes_avg_times = defaultdict(list) ip2host = { '127.0.0.1' : 'localhost' } for host in args.hostnames: logger.info("Processing %s", host) lines = get_data(host) if lines == None: continue try: parse_result(lines) except Exception as e: logger.error("%s", e) print make_result(args) if not args.single_result: data_store = { 'route' : route, 'ip2host' : ip2host, 'routes' : routes, 'routes_avg_times': routes_avg_times } yaml.dump(data_store, open('traceroute.yaml','w'))