summaryrefslogtreecommitdiffstats
path: root/retiolum/scripts/adv_graphgen/tinc_graphs
diff options
context:
space:
mode:
Diffstat (limited to 'retiolum/scripts/adv_graphgen/tinc_graphs')
-rwxr-xr-xretiolum/scripts/adv_graphgen/tinc_graphs/Availability.py61
-rw-r--r--retiolum/scripts/adv_graphgen/tinc_graphs/BackwardsReader.py35
-rwxr-xr-xretiolum/scripts/adv_graphgen/tinc_graphs/Geo.py78
-rwxr-xr-xretiolum/scripts/adv_graphgen/tinc_graphs/Graph.py265
-rwxr-xr-xretiolum/scripts/adv_graphgen/tinc_graphs/Log2JSON.py119
-rw-r--r--retiolum/scripts/adv_graphgen/tinc_graphs/Services.py25
-rwxr-xr-xretiolum/scripts/adv_graphgen/tinc_graphs/Supernodes.py70
-rw-r--r--retiolum/scripts/adv_graphgen/tinc_graphs/__init__.py1
-rw-r--r--retiolum/scripts/adv_graphgen/tinc_graphs/static/map.html88
9 files changed, 742 insertions, 0 deletions
diff --git a/retiolum/scripts/adv_graphgen/tinc_graphs/Availability.py b/retiolum/scripts/adv_graphgen/tinc_graphs/Availability.py
new file mode 100755
index 00000000..888335a7
--- /dev/null
+++ b/retiolum/scripts/adv_graphgen/tinc_graphs/Availability.py
@@ -0,0 +1,61 @@
+#!/usr/bin/python
+# TODO: Rewrite this shitty piece of software ...
+# -*- coding: utf8 -*-
+
+import sys,json,os
+""" TODO: Refactoring needed to pull the edges out of the node structures again,
+it should be easier to handle both structures"""
+DUMP_FILE = os.environ.get("AVAILABILITY_FILE","tinc-availability.json")
+hostpath=os.environ.get("TINC_HOSTPATH", "/etc/tinc/retiolum/hosts")
+
+def get_all_nodes():
+ return os.listdir(hostpath)
+
+def generate_stats():
+ """ Generates availability statistics of the network and nodes
+ """
+ import json
+ jlines = []
+ try:
+ f = open(DUMP_FILE,'r+')
+ for line in f:
+ jlines.append(json.loads(line))
+ f.close()
+ except Exception as e:
+ print("Unable to open and parse Availability DB: {} (override with AVAILABILITY_FILE)".format(DUMP_FILE))
+ sys.exit(1)
+
+ all_nodes = {}
+ for k in get_all_nodes():
+ all_nodes[k] = get_node_availability(k,jlines)
+ print (json.dumps(all_nodes))
+
+def get_node_availability(name,jlines):
+ """ calculates the node availability by reading the generated dump file
+ adding together the uptime of the node and returning the time
+ parms:
+ name - node name
+ jlines - list of already parsed dictionaries node archive
+ """
+ begin = last = current = 0
+ uptime = 0
+ for stat in jlines:
+ if not stat['nodes']:
+ continue
+ ts = stat['timestamp']
+ if not begin:
+ begin = last = ts
+ current = ts
+ if stat['nodes'].get(name,{}).get('to',[]):
+ uptime += current - last
+ else:
+ pass
+ last = ts
+ all_the_time = last - begin
+ try:
+ return uptime/ all_the_time
+ except:
+ return 1
+
+if __name__ == "__main__":
+ generate_stats()
diff --git a/retiolum/scripts/adv_graphgen/tinc_graphs/BackwardsReader.py b/retiolum/scripts/adv_graphgen/tinc_graphs/BackwardsReader.py
new file mode 100644
index 00000000..6bdbf43c
--- /dev/null
+++ b/retiolum/scripts/adv_graphgen/tinc_graphs/BackwardsReader.py
@@ -0,0 +1,35 @@
+import sys
+import os
+import string
+
+class BackwardsReader:
+ """ Stripped and stolen from : http://code.activestate.com/recipes/120686-read-a-text-file-backwards/ """
+ def readline(self):
+ while len(self.data) == 1 and ((self.blkcount * self.blksize) < self.size):
+ self.blkcount = self.blkcount + 1
+ line = self.data[0]
+ try:
+ self.f.seek(-self.blksize * self.blkcount, 2)
+ self.data = string.split(self.f.read(self.blksize) + line, '\n')
+ except IOError:
+ self.f.seek(0)
+ self.data = string.split(self.f.read(self.size - (self.blksize * (self.blkcount-1))) + line, '\n')
+
+ if len(self.data) == 0:
+ return ""
+
+ line = self.data[-1]
+ self.data = self.data[:-1]
+ return line + '\n'
+
+ def __init__(self, file, blksize=4096):
+ """initialize the internal structures"""
+ self.size = os.stat(file)[6]
+ self.blksize = blksize
+ self.blkcount = 1
+ self.f = open(file, 'rb')
+ if self.size > self.blksize:
+ self.f.seek(-self.blksize * self.blkcount, 2)
+ self.data = string.split(self.f.read(self.blksize), '\n')
+ if not self.data[-1]:
+ self.data = self.data[:-1]
diff --git a/retiolum/scripts/adv_graphgen/tinc_graphs/Geo.py b/retiolum/scripts/adv_graphgen/tinc_graphs/Geo.py
new file mode 100755
index 00000000..bfa4ee56
--- /dev/null
+++ b/retiolum/scripts/adv_graphgen/tinc_graphs/Geo.py
@@ -0,0 +1,78 @@
+#!/usr/bin/python3
+# -*- coding: utf8 -*-
+import sys,json,os
+from .Graph import delete_unused_nodes,resolve_myself
+GEODB=os.environ.get("GEODB","GeoLiteCity.dat")
+
+def copy_map():
+ from shutil import copy
+ from os.path import dirname,join,realpath
+ if len(sys.argv) != 2 or sys.argv[1] == "--help" :
+ print("usage: {} <destination>".format(sys.argv[0]))
+ print(" copies the map.html file to the <destination>")
+ sys.exit(1)
+ dstdir=sys.argv[1]
+ copy(realpath(join(dirname(__file__),'static/map.html')),dstdir)
+
+
+def add_geo(nodes):
+ from pygeoip import GeoIP
+ gi = GeoIP(GEODB)
+
+ for k,v in nodes.items():
+ try:
+ nodes[k].update(gi.record_by_addr(v["external-ip"]))
+ except Exception as e:
+ sys.stderr.write(str(e))
+ sys.stderr.write("Cannot determine GeoData for %s\n"%k)
+ return nodes
+
+def add_coords_to_edges(nodes):
+ from pygeoip import GeoIP
+ gi = GeoIP(GEODB)
+
+ for k,v in nodes.items():
+ for i,j in enumerate(v.get("to",[])):
+ data=gi.record_by_addr(j["addr"])
+ try:
+ j["latitude"]=data["latitude"]
+ j["longitude"]=data["longitude"]
+ except Exception as e: pass
+
+ return nodes
+
+def add_jitter(nodes):
+ from random import random
+ #add a bit of jitter to all of the coordinates
+ max_jitter=0.005
+ for k,v in nodes.items():
+ jitter_lat= max_jitter -random()*max_jitter*2
+ jitter_long= max_jitter -random()*max_jitter*2
+ try:
+ v["latitude"]= v["latitude"] + jitter_lat
+ v["longitude"]= v["longitude"] + jitter_long
+ for nodek,node in nodes.items():
+ for to in node['to']:
+ if to['name'] == k:
+ to['latitude'] = v["latitude"]
+ to['longitude'] = v["longitude"]
+ except Exception as e: pass
+ return nodes
+
+def main():
+ import json
+ try:
+ with open(GEODB,'rb') as f: f.read()
+ except Exception as e:
+ print("cannot open {} (GEODB in env)".format(GEODB))
+ print(e)
+ sys.exit(1)
+ try:
+ nodes = add_jitter(add_coords_to_edges(add_geo(resolve_myself(delete_unused_nodes(json.load(sys.stdin))))))
+ print (json.dumps(nodes))
+ except Exception as e:
+ print("cannot parse data received via stdin")
+ print(e)
+
+if __name__ == "__main__":
+ main()
diff --git a/retiolum/scripts/adv_graphgen/tinc_graphs/Graph.py b/retiolum/scripts/adv_graphgen/tinc_graphs/Graph.py
new file mode 100755
index 00000000..29491997
--- /dev/null
+++ b/retiolum/scripts/adv_graphgen/tinc_graphs/Graph.py
@@ -0,0 +1,265 @@
+#!/usr/bin/python
+from .BackwardsReader import BackwardsReader
+import sys,json,os
+from .Supernodes import check_all_the_super
+from .Services import add_services
+from .Availability import get_node_availability
+import sys,json
+from time import time
+DUMP_FILE = os.environ.get("AVAILABILITY_FILE", "tinc-availability.json")
+hostpath=os.environ.get("TINC_HOSTPATH", "/etc/tinc/retiolum/hosts")
+
+# will be filled later
+supernodes= []
+
+def resolve_myself(nodes):
+ #resolve MYSELF to the real ip
+ for k,v in nodes.items():
+ if v["external-ip"] == "MYSELF":
+ for nodek,node in nodes.items():
+ for to in node['to']:
+ if to['name'] == k:
+ v["external-ip"] = to["addr"]
+ return nodes
+
+
+def dump_graph(nodes):
+ from time import time
+ graph = {}
+ graph['nodes'] = nodes
+ graph['timestamp'] = time()
+ f = open(DUMP_FILE,'a')
+ json.dump(graph,f)
+ f.write('\n')
+ f.close()
+
+def generate_availability_stats(nodes):
+ """ generates stats of from availability
+ """
+ jlines = []
+ # try:
+ # f = BackwardsReader(DUMP_FILE)
+ # lines_to_use = 1000
+ # while True:
+ # if lines_to_use == 0: break
+ # line = f.readline()
+ # if not line: break
+ # jline = json.loads(line)
+ # if not jline['nodes']: continue
+
+ # jlines.append(jline)
+ # lines_to_use -=1
+ # except Exception as e: sys.stderr.write(str(e))
+
+ for k,v in nodes.items():
+ # TODO: get this information in a different way
+ v['availability'] = get_node_availability(k,[])
+
+
+def generate_stats(nodes):
+ """ Generates some statistics of the network and nodes
+ """
+ for k,v in nodes.items():
+ conns = v.get('to',[])
+ for c in conns: #sanitize weights
+ if float(c['weight']) > 9000: c['weight'] = str(9001)
+ elif float(c['weight']) < 0: c['weight'] = str(0)
+ v['num_conns'] = len(conns)
+ v['avg_weight'] = get_node_avg_weight(conns)
+
+def get_node_avg_weight(conns):
+ """ calculates the average weight for the given connections """
+ if not conns:
+ sys.syderr.write("get_node_avg_weight: connection parameter empty")
+ return 9001
+ else:
+ return sum([float(c['weight']) for c in conns])/len(conns)
+
+def delete_unused_nodes(nodes):
+ """ Deletes all the nodes which are currently not connected to the network"""
+ new_nodes = {}
+ for k,v in nodes.items():
+ if v['external-ip'] == "(null)":
+ continue
+ if v.get('to',[]):
+ new_nodes[k] = v
+ for k,v in new_nodes.items():
+ if not [ i for i in v['to'] if i['name'] in new_nodes]:
+ del(k)
+ return new_nodes
+
+def merge_edges(nodes):
+ """ merge back and forth edges into one
+ DESTRUCTS the current structure by deleting "connections" in the nodes
+ """
+ for k,v in nodes.items():
+ for con in v.get('to',[]):
+ for i,secon in enumerate(nodes.get(con['name'],{}).get('to',[])):
+ if k == secon['name']:
+ del (nodes[con['name']]['to'][i])
+ con['bidirectional'] = True
+
+
+def print_head():
+ print ('digraph retiolum {')
+ print (' graph [center=true packMode="clust"]')
+ print (' node[shape=box,style=filled,fillcolor=grey]')
+ print (' overlap=false')
+
+def print_stat_node(nodes):
+ ''' Write a `stats` node in the corner
+ This node contains infos about the current number of active nodes and connections inside the network
+ '''
+ from time import localtime,strftime
+ num_conns = 0
+ num_nodes = len(nodes)
+ try:
+ msg = '%s.num_nodes %d %d\r\n' %(g_path,num_nodes,begin)
+ s.send(msg)
+ except Exception as e: pass
+ for k,v in nodes.items():
+ num_conns+= len(v['to'])
+ node_text = " stats_node [label=\"Statistics\\l"
+ node_text += "Build Date : %s\\l" % strftime("%Y-%m-%d %H:%M:%S",localtime())
+ node_text += "Active Nodes: %s\\l" % num_nodes
+ node_text += "Connections : %s\\l" % num_conns
+ node_text += "\""
+ node_text += ",fillcolor=green"
+ node_text += "]"
+ print(node_text)
+
+def print_node(k,v):
+ """ writes a single node and its edges
+ edges are weightet with the informations inside the nodes provided by
+ tinc
+ """
+
+ node = " "+k+"[label=<<TABLE border='0' title='%s' cellborder='1' >" %k
+ node += "<TR><TD colspan='2'><B>%s</B></TD></TR>"%k
+ if 'availability' in v:
+ node += "<TR><TD>availability:</TD><TD>%f</TD></TR>" % v['availability']
+
+ if 'num_conns' in v:
+ node += "<TR><TD>Num Connects:</TD><TD>%s</TD></TR>"%str(v['num_conns'])
+
+ node += "<TR><TD>external:</TD><TD>"+v['external-ip']+":"+v['external-port']+"</TD></TR>"
+ for addr in v.get('internal-ip',['dunno lol']):
+ node += "<TR><TD>internal:</TD><TD>%s</TD></TR>"%addr
+
+ if 'services' in v:
+ node +="<TR><TD colspan='2'><B>Services:</B></TD></TR>"
+ for service in v['services']:
+ try:uri,comment = service.split(" ",1)
+ except:
+ uri = service
+ comment =""
+ node +="<TR >"
+ uri_proto=uri.split(":")[0]
+ uri_rest = uri.split(":")[1]
+ if not uri_rest:
+ node +="<TD title='{0}' align='left' colspan='2' \
+href='{0}'><font color='darkred'>{0}</font>".format(uri)
+ else:
+ node +="<TD title='{0}' align='left' colspan='2' \
+href='{0}'><U>{0}</U>".format(uri)
+ if comment:
+ node += "<br align='left'/> <I>{0}</I>".format(comment)
+ node +="</TD></TR>"
+ # end label
+ node +="</TABLE>>"
+
+ if v['num_conns'] == 1:
+ node += ",fillcolor=red"
+ elif k in supernodes:
+ node += ",fillcolor=steelblue1"
+ node += "]"
+
+ print(node)
+
+def print_anonymous_node(k,v):
+ """ writes a single node and its edges
+ edges are weightet with the informations inside the nodes provided by
+ tinc
+ """
+
+ node = " "+k #+"[label=\""
+ print(node)
+
+def print_edge(k,v):
+ for con in v.get('to',[]):
+ label = con['weight']
+ w = int(con['weight'])
+ weight = str(1000 - (((w - 150) * (1000 - 0)) / (1000 -150 )) + 0)
+
+ length = str(float(w)/1500)
+ if float(weight) < 0 :
+ weight= "1"
+
+ edge = " "+k+ " -> " +con['name'] + " [label="+label + " weight="+weight
+ if con.get('bidirectional',False):
+ edge += ",dir=both"
+ edge += "]"
+ print(edge)
+
+def anonymize_nodes(nodes):
+ #anonymizes all nodes
+ i = "0"
+ newnodes = {}
+ for k,v in nodes.items():
+ for nodek,node in nodes.items():
+ for to in node['to']:
+ if to['name'] == k:
+ to['name'] = i
+ newnodes[i] = v
+ i = str(int(i)+1)
+ return newnodes
+
+def main():
+ if len(sys.argv) != 2 or sys.argv[1] not in ["anonymous","complete"]:
+ print("usage: %s (anonymous|complete)")
+ sys.exit(1)
+
+ nodes = json.load(sys.stdin)
+ nodes = delete_unused_nodes(nodes)
+ print_head()
+ generate_stats(nodes)
+ merge_edges(nodes)
+
+
+ if sys.argv[1] == "anonymous":
+ nodes = anonymize_nodes(nodes)
+
+ for k,v in nodes.items():
+ print_anonymous_node(k,v)
+ print_edge(k,v)
+
+ elif sys.argv[1] == "complete":
+ try:
+ for supernode,addr in check_all_the_super(hostpath):
+ supernodes.append(supernode)
+ except FileNotFoundError as e:
+ print("!! cannot load list of supernodes ({})".format(hostpath))
+ print("!! Use TINC_HOSTPATH env to override")
+ sys.exit(1)
+
+ generate_availability_stats(nodes)
+ add_services(nodes)
+ for k,v in nodes.items():
+ print_node(k,v)
+ print_edge(k,v)
+
+ #TODO: get availability somehow else
+ # try:
+ # dump_graph(nodes)
+ # except Exception as e:
+ # sys.stderr.write("Cannot dump graph: %s" % str(e))
+ else:
+ pass
+
+ print_stat_node(nodes)
+ print ('}')
+
+if __name__ == "__main__":
+ main()
+
+# vim: set sw=2:ts=2
diff --git a/retiolum/scripts/adv_graphgen/tinc_graphs/Log2JSON.py b/retiolum/scripts/adv_graphgen/tinc_graphs/Log2JSON.py
new file mode 100755
index 00000000..b0bc209b
--- /dev/null
+++ b/retiolum/scripts/adv_graphgen/tinc_graphs/Log2JSON.py
@@ -0,0 +1,119 @@
+#!/usr/bin/python
+import subprocess
+import os
+import re
+import sys
+import json
+
+
+TINC_NETWORK =os.environ.get("TINC_NETWORK","retiolum")
+
+# is_legacy is the parameter which defines if the tinc config files are handled old fashioned (parse from syslog),
+# or if the new and hip tincctl should be used
+
+
+# Tags and Delimiters
+TINC_TAG="tinc.%s" % TINC_NETWORK
+BEGIN_NODES = "Nodes:"
+END_NODES = "End of nodes."
+BEGIN_SUBNET = "Subnet list:"
+END_SUBNET = "End of subnet list"
+BEGIN_EDGES = "Edges:"
+END_EDGES = "End of edges."
+def usage():
+ from sys import argv,exit
+ print("""usage: %s
+This tool dumps all tinc node informations as json
+
+ENVIRONMENT VARIABLES:
+ TINC_NETWORK The tinc network to dump
+ (default: retiolum)
+""" % argv[0])
+ exit(1)
+
+def debug(func):
+ from functools import wraps
+ @wraps(func)
+ def with_debug(*args,**kwargs):
+ print( func.__name__ + " (args: %s | kwargs %s)"% (args,kwargs))
+ return func(*args,**kwargs)
+ return with_debug
+
+
+def parse_tinc_stats():
+ import subprocess
+ from time import sleep
+ from distutils.spawn import find_executable as which
+ #newest tinc
+ if which("tinc"):
+ return parse_new_input("tinc")
+ #new tinc
+ elif which("tincctl"):
+ return parse_new_input("tincctl")
+ #old tinc
+ elif which("tincd"):
+ print("old tincd not supported")
+ sys.exit(1)
+ else:
+ raise Exception("no tinc executable found!")
+
+
+def parse_new_input(tinc_bin):
+ nodes = {}
+ pnodes = subprocess.Popen(
+ [tinc_bin,"-n",TINC_NETWORK,"dump","reachable","nodes"],
+ stdout=subprocess.PIPE).communicate()[0].decode()
+ for line in pnodes.split('\n'):
+ if not line: continue
+ l = line.split()
+ n = l[0]
+
+ token = l[1]
+ if token == 'id':
+ # new format
+ # <name> id <ident> at <ip> port <port>
+ ident = l[1]
+ l = l[2:] #shift over 'id <ident>'
+ # else: # token = 'at'
+ # old format:
+ # <name> at <ip> port <port>
+ _,_,ip,_,port = l[:5]
+ nodes[n]= { 'external-ip': ip, 'external-port' : l[4] }
+
+ psubnets = subprocess.check_output(
+ [tinc_bin,"-n",TINC_NETWORK,"dump","subnets"]).decode()
+ for line in psubnets.split('\n'):
+ if not line: continue
+ l = line.split()
+ try:
+ if not nodes[l[2]].get('internal-ip',False):
+ nodes[l[2]]['internal-ip'] = []
+ nodes[l[2]]['internal-ip'].append(l[0].split('#')[0])
+ except KeyError:
+ pass # node does not exist (presumably)
+
+ pedges = subprocess.check_output(
+ [tinc_bin,"-n",TINC_NETWORK,"dump","edges"]).decode()
+ for line in pedges.split('\n'):
+ if not line: continue
+ l = line.split()
+ # TODO: tokenize this and parse the line
+ n = l[0]
+ try:
+ if not 'to' in nodes[l[0]] :
+ nodes[n]['to'] = []
+ nodes[n]['to'].append(
+ {'name':l[2],'addr':l[4],'port':l[6],'weight' : l[-1] })
+ except KeyError:
+ pass #node does not exist
+ return nodes
+
+def main():
+ from sys import argv
+ if len(argv) > 1:
+ usage()
+ else:
+ print (json.dumps(parse_tinc_stats()))
+
+if __name__ == '__main__':
+ main()
diff --git a/retiolum/scripts/adv_graphgen/tinc_graphs/Services.py b/retiolum/scripts/adv_graphgen/tinc_graphs/Services.py
new file mode 100644
index 00000000..9581e21e
--- /dev/null
+++ b/retiolum/scripts/adv_graphgen/tinc_graphs/Services.py
@@ -0,0 +1,25 @@
+import os,sys
+services_dir=os.environ.get("SERIVCES_DIR","/home/reaktor/nag.services")
+def add_services(nodes):
+ for k,v in nodes.items():
+ n = nodes[k]
+ try:
+ with open("{0}/{1}".format(services_dir,k)) as f:
+ n["services"] = []
+ for line in f.readlines():
+ n["services"].append(line.strip())
+ except Exception as e:
+ n["services"] = ["Error: No Service File!"]
+ return nodes
+def main():
+ import json,sys
+ try:
+ nodes = add_services(json.load(sys.stdin))
+ print (json.dumps(nodes,indent=4))
+ except:
+ print("unable to parse json data from stdin")
+ sys.exit(1)
+
+if __name__ == "__main__":
+ main()
+# vim: set expandtab:ts=4:sw=4
diff --git a/retiolum/scripts/adv_graphgen/tinc_graphs/Supernodes.py b/retiolum/scripts/adv_graphgen/tinc_graphs/Supernodes.py
new file mode 100755
index 00000000..bc66b337
--- /dev/null
+++ b/retiolum/scripts/adv_graphgen/tinc_graphs/Supernodes.py
@@ -0,0 +1,70 @@
+#!/usr/bin/python3
+
+def find_potential_super(path="/etc/tinc/retiolum/hosts"):
+ import os
+ import re
+
+ needle_addr = re.compile("Address\s*=\s*(.*)")
+ needle_port = re.compile("Port\s*=\s*(.*)")
+ for f in os.listdir(path):
+ try:
+ with open(path+"/"+f) as of:
+ addrs = []
+ port = "655"
+
+ for line in of.readlines():
+
+ addr_found = needle_addr.match(line)
+ if addr_found:
+ addrs.append(addr_found.group(1))
+
+ port_found = needle_port.match(line)
+ if port_found:
+ port = port_found.group(1)
+
+ if addrs : yield (f ,[(addr ,int(port)) for addr in addrs])
+ except FileNotFoundError as e:
+ print("Cannot open hosts directory to be used to find potential supernodes")
+ print("Directory used: {}".format(path))
+ raise
+
+
+def try_connect(addr):
+ try:
+ from socket import socket,AF_INET,SOCK_STREAM
+ s = socket(AF_INET,SOCK_STREAM)
+ s.settimeout(2)
+ s.connect(addr)
+ s.settimeout(None)
+ s.close()
+ return addr
+ except Exception as e:
+ pass
+
+
+def check_one_super(ha):
+ host,addrs = ha
+ valid_addrs = []
+ for addr in addrs:
+ ret = try_connect(addr)
+ if ret: valid_addrs.append(ret)
+ if valid_addrs: return (host,valid_addrs)
+
+
+def check_all_the_super(path):
+ from multiprocessing import Pool
+ p = Pool(20)
+ return filter(None,p.map(check_one_super,find_potential_super(path)))
+
+
+def main():
+ import os
+ hostpath=os.environ.get("TINC_HOSTPATH", "/etc/tinc/retiolum/hosts")
+
+ for host,addrs in check_all_the_super(hostpath):
+ print("%s %s" %(host,str(addrs)))
+
+if __name__ == "__main__":
+ main()
+
+# vim: set expandtab:ts=:sw=2
diff --git a/retiolum/scripts/adv_graphgen/tinc_graphs/__init__.py b/retiolum/scripts/adv_graphgen/tinc_graphs/__init__.py
new file mode 100644
index 00000000..414ffe99
--- /dev/null
+++ b/retiolum/scripts/adv_graphgen/tinc_graphs/__init__.py
@@ -0,0 +1 @@
+__version__="0.2.3"
diff --git a/retiolum/scripts/adv_graphgen/tinc_graphs/static/map.html b/retiolum/scripts/adv_graphgen/tinc_graphs/static/map.html
new file mode 100644
index 00000000..ef8a0565
--- /dev/null
+++ b/retiolum/scripts/adv_graphgen/tinc_graphs/static/map.html
@@ -0,0 +1,88 @@
+
+
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Simple Map</title>
+ <meta name="viewport" content="initial-scale=1.0, user-scalable=no">
+ <meta charset="utf-8">
+ <style>
+ html, body, #map-canvas {
+ margin: 0;
+ padding: 0;
+ height: 100%;
+ }
+ </style>
+ <script src="https://maps.googleapis.com/maps/api/js?v=3.exp&sensor=false"></script>
+ <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.7/jquery.min.js"></script>
+ <script type="text/javascript" src="jquery.ui.map.js"></script>
+ <script>
+ var map;
+ var markerlist ={};
+ function addInfoWindow(marker, message) {
+ var info = message;
+
+ var infoWindow = new google.maps.InfoWindow({
+ content: message
+ });
+
+ google.maps.event.addListener(marker, 'click', function () {
+
+ // close all the infowindows
+ $.each(markerlist,function (k,v){
+ v["infowin"].close()
+ });
+ infoWindow.open(map, marker);
+ });
+ return infoWindow;
+ }
+ function initialize() {
+ var mapOptions = {
+ zoom: 5,
+ center: new google.maps.LatLng(51,9),
+ mapTypeId: google.maps.MapTypeId.ROADMAP
+ };
+ map = new google.maps.Map(document.getElementById('map-canvas'),
+ mapOptions);
+
+ $.getJSON("marker.json",function (data){
+ $.each(data,function (k,v) {
+
+ // add initial marker
+ var pt=new google.maps.LatLng(v["latitude"],v["longitude"])
+ var marker= new google.maps.Marker({
+ id: k,map:map,title: k,position:pt,content:k})
+
+ // add edges
+ $.each(v["to"],function(iter,val){
+ var line = [
+ pt,
+ new google.maps.LatLng(val['latitude'],val['longitude'])
+ ]
+
+ var connector = new google.maps.Polyline({
+ path:line,
+ strokeColor: "#FF0000",
+ strokeOpacity: 1.0,
+ strokeWeight: 2
+ })
+ connector.setMap(map);
+ val["line"]=connector
+
+ })
+
+
+ marker["infowin"]=addInfoWindow(marker,k+'<br/>'+v["city"]);
+ markerlist[k] = marker;
+ });
+ })
+ };
+
+ google.maps.event.addDomListener(window, 'load', initialize);
+
+ </script>
+ </head>
+ <body>
+ <div id="map-canvas"></div>
+ </body>
+</html>