diff --git a/GlobalRRD.py b/GlobalRRD.py index f3f3960..b3cf31a 100644 --- a/GlobalRRD.py +++ b/GlobalRRD.py @@ -2,6 +2,7 @@ import os import subprocess from RRD import RRD, DS, RRA + class GlobalRRD(RRD): ds_list = [ # Number of nodes available @@ -10,14 +11,17 @@ class GlobalRRD(RRD): DS('clients', 'GAUGE', 120, 0, float('NaN')), ] rra_list = [ - RRA('AVERAGE', 0.5, 1, 120), # 2 hours of 1 minute samples - RRA('AVERAGE', 0.5, 60, 744), # 31 days of 1 hour samples - RRA('AVERAGE', 0.5, 1440, 1780),# ~5 years of 1 day samples + # 2 hours of 1 minute samples + RRA('AVERAGE', 0.5, 1, 120), + # 31 days of 1 hour samples + RRA('AVERAGE', 0.5, 60, 744), + # ~5 years of 1 day samples + RRA('AVERAGE', 0.5, 1440, 1780), ] def __init__(self, directory): super().__init__(os.path.join(directory, "nodes.rrd")) - self.ensureSanity(self.ds_list, self.rra_list, step=60) + self.ensure_sanity(self.ds_list, self.rra_list, step=60) def update(self, nodeCount, clientCount): super().update({'nodes': nodeCount, 'clients': clientCount}) @@ -30,6 +34,5 @@ class GlobalRRD(RRD): 'DEF:nodes=' + self.filename + ':nodes:AVERAGE', 'LINE1:nodes#F00:nodes\\l', 'DEF:clients=' + self.filename + ':clients:AVERAGE', - 'LINE2:clients#00F:clients', - ] + 'LINE2:clients#00F:clients'] subprocess.check_output(args) diff --git a/NodeRRD.py b/NodeRRD.py index ca24c0d..a4ec092 100644 --- a/NodeRRD.py +++ b/NodeRRD.py @@ -2,19 +2,24 @@ import os import subprocess from RRD import RRD, DS, RRA + class NodeRRD(RRD): ds_list = [ DS('upstate', 'GAUGE', 120, 0, 1), DS('clients', 'GAUGE', 120, 0, float('NaN')), ] rra_list = [ - RRA('AVERAGE', 0.5, 1, 120), # 2 hours of 1 minute samples - RRA('AVERAGE', 0.5, 5, 1440), # 5 days of 5 minute samples - RRA('AVERAGE', 0.5, 60, 720), # 30 days of 1 hour samples - RRA('AVERAGE', 0.5, 720, 730), # 1 year of 12 hour samples + # 2 hours of 1 minute samples + RRA('AVERAGE', 0.5, 1, 120), + # 5 days of 5 minute samples + RRA('AVERAGE', 0.5, 5, 1440), + # 30 days of 1 hour samples + RRA('AVERAGE', 0.5, 60, 720), + # 1 year of 12 hour samples + RRA('AVERAGE', 0.5, 720, 730), ] - def __init__(self, filename, node = None): + def __init__(self, filename, node=None): """ Create a new RRD for a given node. @@ -22,12 +27,13 @@ class NodeRRD(RRD): """ self.node = node super().__init__(filename) - self.ensureSanity(self.ds_list, self.rra_list, step=60) + self.ensure_sanity(self.ds_list, self.rra_list, step=60) @property def imagename(self): - return os.path.basename(self.filename).rsplit('.', 2)[0] + ".png" + return "{basename}.png".format(basename=os.path.basename(self.filename).rsplit('.', 2)[0]) + # TODO: fix this, python does not support function overloading def update(self): super().update({'upstate': int(self.node['flags']['online']), 'clients': self.node['statistics']['clients']}) @@ -36,8 +42,8 @@ class NodeRRD(RRD): Create a graph in the given directory. The file will be named basename.png if the RRD file is named basename.rrd """ - args = ['rrdtool','graph', os.path.join(directory, self.imagename), - '-s', '-' + timeframe , + args = ['rrdtool', 'graph', os.path.join(directory, self.imagename), + '-s', '-' + timeframe, '-w', '800', '-h', '400', '-l', '0', @@ -48,6 +54,5 @@ class NodeRRD(RRD): 'CDEF:d=clients,UN,maxc,UN,1,maxc,IF,*', 'AREA:c#0F0:up\\l', 'AREA:d#F00:down\\l', - 'LINE1:c#00F:clients connected\\l', - ] + 'LINE1:c#00F:clients connected\\l'] subprocess.check_output(args) diff --git a/RRD.py b/RRD.py index 9bb87a0..799338c 100644 --- a/RRD.py +++ b/RRD.py @@ -1,19 +1,20 @@ import subprocess import re -import io import os -from tempfile import TemporaryFile from operator import xor, eq from functools import reduce from itertools import starmap import math + class RRDIncompatibleException(Exception): """ Is raised when an RRD doesn't have the desired definition and cannot be upgraded to it. """ pass + + class RRDOutdatedException(Exception): """ Is raised when an RRD doesn't have the desired definition, but can be @@ -25,7 +26,8 @@ if not hasattr(__builtins__, "FileNotFoundError"): class FileNotFoundError(Exception): pass -class RRD: + +class RRD(object): """ An RRD is a Round Robin Database, a database which forgets old data and aggregates multiple records into new ones. @@ -49,7 +51,7 @@ class RRD: def _exec_rrdtool(self, cmd, *args, **kwargs): pargs = ["rrdtool", cmd, self.filename] - for k,v in kwargs.items(): + for k, v in kwargs.items(): pargs.extend(["--" + k, str(v)]) pargs.extend(args) subprocess.check_output(pargs) @@ -57,7 +59,7 @@ class RRD: def __init__(self, filename): self.filename = filename - def ensureSanity(self, ds_list, rra_list, **kwargs): + def ensure_sanity(self, ds_list, rra_list, **kwargs): """ Create or upgrade the RRD file if necessary to contain all DS in ds_list. If it needs to be created, the RRAs in rra_list and any kwargs @@ -65,13 +67,13 @@ class RRD: database are NOT modified! """ try: - self.checkSanity(ds_list) + self.check_sanity(ds_list) except FileNotFoundError: self.create(ds_list, rra_list, **kwargs) except RRDOutdatedException: self.upgrade(ds_list) - def checkSanity(self, ds_list=()): + def check_sanity(self, ds_list=()): """ Check if the RRD file exists and contains (at least) the DS listed in ds_list. @@ -82,7 +84,8 @@ class RRD: if set(ds_list) - set(info['ds'].values()) != set(): for ds in ds_list: if ds.name in info['ds'] and ds.type != info['ds'][ds.name].type: - raise RRDIncompatibleException("%s is %s but should be %s" % (ds.name, ds.type, info['ds'][ds.name].type)) + raise RRDIncompatibleException("%s is %s but should be %s" % + (ds.name, ds.type, info['ds'][ds.name].type)) else: raise RRDOutdatedException() @@ -106,7 +109,7 @@ class RRD: old_ds = info['ds'][ds.name] if info['ds'][ds.name].type != ds.type: raise RuntimeError('Cannot convert existing DS "%s" from type "%s" to "%s"' % - (ds.name, old_ds.type, ds.type)) + (ds.name, old_ds.type, ds.type)) ds.index = old_ds.index new_ds[ds.index] = ds else: @@ -116,12 +119,11 @@ class RRD: dump = subprocess.Popen( ["rrdtool", "dump", self.filename], - stdout=subprocess.PIPE - ) + stdout=subprocess.PIPE) + restore = subprocess.Popen( ["rrdtool", "restore", "-", self.filename + ".new"], - stdin=subprocess.PIPE - ) + stdin=subprocess.PIPE) echo = True ds_definitions = True for line in dump.stdout: @@ -143,16 +145,14 @@ class RRD: %s %i - """ % ( - ds.name, - ds.type, - ds.args[0], - ds.args[1], - ds.args[2], - ds.last_ds, - ds.value, - ds.unknown_sec) - , "utf-8")) + """ % (ds.name, + ds.type, + ds.args[0], + ds.args[1], + ds.args[2], + ds.last_ds, + ds.value, + ds.unknown_sec), "utf-8")) if b'' in line: restore.stdin.write(added_ds_num*b""" @@ -272,7 +272,8 @@ class RRD: self._cached_info = info return info -class DS: + +class DS(object): """ DS stands for Data Source and represents one line of data points in a Round Robin Database (RRD). @@ -284,6 +285,7 @@ class DS: last_ds = 'U' value = 0 unknown_sec = 0 + def __init__(self, name, dst, *args): self.name = name self.type = dst @@ -293,7 +295,7 @@ class DS: return "DS:%s:%s:%s" % ( self.name, self.type, - ":".join(map(str, self._nan_to_U_args())) + ":".join(map(str, self._nan_to_u_args())) ) def __repr__(self): @@ -305,22 +307,23 @@ class DS: ) def __eq__(self, other): - return all(starmap(eq, zip(self._compare_keys(), other._compare_keys()))) + return all(starmap(eq, zip(self.compare_keys(), other.compare_keys()))) def __hash__(self): - return reduce(xor, map(hash, self._compare_keys())) + return reduce(xor, map(hash, self.compare_keys())) - def _nan_to_U_args(self): + def _nan_to_u_args(self): return tuple( 'U' if type(arg) is float and math.isnan(arg) else arg for arg in self.args ) - def _compare_keys(self): - return (self.name, self.type, self._nan_to_U_args()) + def compare_keys(self): + return self.name, self.type, self._nan_to_u_args() -class RRA: + +class RRA(object): def __init__(self, cf, *args): self.cf = cf self.args = args diff --git a/alfred.py b/alfred.py index 878ac0f..d334656 100644 --- a/alfred.py +++ b/alfred.py @@ -1,15 +1,19 @@ import subprocess import json + def _fetch(data_type): - output = subprocess.check_output(["alfred-json", "-z", "-f", "json", "-r", str(data_type)]) - return json.loads(output.decode("utf-8")).values() + output = subprocess.check_output(["alfred-json", "-z", "-f", "json", "-r", str(data_type)]) + return json.loads(output.decode("utf-8")).values() + def nodeinfo(): - return _fetch(158) + return _fetch(158) + def statistics(): - return _fetch(159) + return _fetch(159) + def vis(): - return _fetch(160) + return _fetch(160) diff --git a/backend.py b/backend.py index 7544709..ff943ec 100755 --- a/backend.py +++ b/backend.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 - +""" +backend.py - ffmap-backend runner +https://github.com/ffnord/ffmap-backend +""" import argparse import json import os -import sys import networkx as nx from datetime import datetime from networkx.readwrite import json_graph @@ -11,91 +13,89 @@ from networkx.readwrite import json_graph import alfred import nodes import graph -from batman import batman -from rrddb import rrd +from batman import Batman +from rrddb import RRD -parser = argparse.ArgumentParser() -parser.add_argument('-a', '--aliases', - help='read aliases from FILE', - default=[], - action='append', - metavar='FILE') +def main(params): + if not params['mesh']: + params['mesh'] = ['bat0'] -parser.add_argument('-m', '--mesh', action='append', - help='batman mesh interface') + nodes_fn = os.path.join(params['destination_directory'], 'nodes.json') + graph_fn = os.path.join(params['destination_directory'], 'graph.json') -parser.add_argument('-d', '--destination-directory', action='store', - help='destination directory for generated files',required=True) + now = datetime.utcnow().replace(microsecond=0) -parser.add_argument('--vpn', action='append', metavar='MAC', - help='assume MAC to be part of the VPN') + with open(nodes_fn, 'r') as nodedb_handle: + nodedb = json.load(nodedb_handle) -parser.add_argument('--prune', metavar='DAYS', - help='forget nodes offline for at least DAYS') + # flush nodedb if it uses the old format + if 'links' in nodedb: + nodedb = {'nodes': dict()} -args = parser.parse_args() + nodedb['timestamp'] = now.isoformat() -options = vars(args) + for node_id, node in nodedb['nodes'].items(): + node['flags']['online'] = False -if not options['mesh']: - options['mesh'] = ['bat0'] + nodes.import_nodeinfo(nodedb['nodes'], alfred.nodeinfo(), now, assume_online=True) -nodes_fn = os.path.join(options['destination_directory'], 'nodes.json') -graph_fn = os.path.join(options['destination_directory'], 'graph.json') + for aliases in params['aliases']: + with open(aliases, 'r') as f: + nodes.import_nodeinfo(nodedb['nodes'], json.load(f), now, assume_online=False) -now = datetime.utcnow().replace(microsecond=0) + nodes.reset_statistics(nodedb['nodes']) + nodes.import_statistics(nodedb['nodes'], alfred.statistics()) -try: - nodedb = json.load(open(nodes_fn)) + bm = list(map(lambda d: (d.vis_data(True), d.gateway_list()), map(Batman, params['mesh']))) + for vis_data, gateway_list in bm: + nodes.import_mesh_ifs_vis_data(nodedb['nodes'], vis_data) + nodes.import_vis_clientcount(nodedb['nodes'], vis_data) + nodes.mark_vis_data_online(nodedb['nodes'], vis_data, now) + nodes.mark_gateways(nodedb['nodes'], gateway_list) - # ignore if old format - if 'links' in nodedb: - raise -except: - nodedb = {'nodes': dict()} + if params['prune']: + nodes.prune_nodes(nodedb['nodes'], now, int(params['prune'])) -nodedb['timestamp'] = now.isoformat() + batadv_graph = nx.DiGraph() + for vis_data, gateway_list in bm: + graph.import_vis_data(batadv_graph, nodedb['nodes'], vis_data) -for node_id, node in nodedb['nodes'].items(): - node['flags']['online'] = False + if params['vpn']: + graph.mark_vpn(batadv_graph, frozenset(params['vpn'])) -nodes.import_nodeinfo(nodedb['nodes'], alfred.nodeinfo(), now, assume_online=True) + batadv_graph = graph.merge_nodes(batadv_graph) + batadv_graph = graph.to_undirected(batadv_graph) -for aliases in options['aliases']: - with open(aliases, 'r') as f: - nodes.import_nodeinfo(nodedb['nodes'], json.load(f), now, assume_online=False) + with open(nodes_fn, 'w') as f: + json.dump(nodedb, f) -nodes.reset_statistics(nodedb['nodes']) -nodes.import_statistics(nodedb['nodes'], alfred.statistics()) + with open(graph_fn, 'w') as f: + json.dump({'batadv': json_graph.node_link_data(batadv_graph)}, f) -bm = list(map(lambda d: (d.vis_data(True), d.gateway_list()), map(batman, options['mesh']))) -for vis_data, gateway_list in bm: - nodes.import_mesh_ifs_vis_data(nodedb['nodes'], vis_data) - nodes.import_vis_clientcount(nodedb['nodes'], vis_data) - nodes.mark_vis_data_online(nodedb['nodes'], vis_data, now) - nodes.mark_gateways(nodedb['nodes'], gateway_list) + scriptdir = os.path.dirname(os.path.realpath(__file__)) + rrd = RRD(scriptdir + '/nodedb/', params['destination_directory'] + '/nodes') + rrd.update_database(nodedb['nodes']) + rrd.update_images() -if options['prune']: - nodes.prune_nodes(nodedb['nodes'], now, int(options['prune'])) -batadv_graph = nx.DiGraph() -for vis_data, gateway_list in bm: - graph.import_vis_data(batadv_graph, nodedb['nodes'], vis_data) +if __name__ == '__main__': + parser = argparse.ArgumentParser() -if options['vpn']: - graph.mark_vpn(batadv_graph, frozenset(options['vpn'])) + parser.add_argument('-a', '--aliases', + help='read aliases from FILE', + default=[], action='append', + metavar='FILE') + parser.add_argument('-m', '--mesh', action='append', + help='batman mesh interface') + parser.add_argument('-d', '--destination-directory', action='store', + help='destination directory for generated files', + required=True) + parser.add_argument('--vpn', action='append', metavar='MAC', + help='assume MAC to be part of the VPN') + parser.add_argument('--prune', metavar='DAYS', + help='forget nodes offline for at least DAYS') -batadv_graph = graph.merge_nodes(batadv_graph) -batadv_graph = graph.to_undirected(batadv_graph) + options = vars(parser.parse_args()) -with open(nodes_fn, 'w') as f: - json.dump(nodedb, f) - -with open(graph_fn, 'w') as f: - json.dump({'batadv': json_graph.node_link_data(batadv_graph)}, f) - -scriptdir = os.path.dirname(os.path.realpath(__file__)) -rrd = rrd(scriptdir + '/nodedb/', options['destination_directory'] + '/nodes') -rrd.update_database(nodedb['nodes']) -rrd.update_images() + main(options) diff --git a/batman.py b/batman.py index 94229ad..86ad4fe 100644 --- a/batman.py +++ b/batman.py @@ -1,82 +1,90 @@ -#!/usr/bin/env python3 import subprocess import json import re -class batman: - """ Bindings for B.A.T.M.A.N. advanced batctl tool - """ - def __init__(self, mesh_interface = "bat0"): - self.mesh_interface = mesh_interface - def vis_data(self,batadv_vis=False): - vds = self.vis_data_batctl_legacy() - if batadv_vis: - vds += self.vis_data_batadv_vis() - return vds - - def vis_data_helper(self,lines): - vd = [] - for line in lines: - try: - utf8_line = line.decode("utf-8") - vd.append(json.loads(utf8_line)) - except e: - pass - return vd - - def vis_data_batctl_legacy(self): - """ Parse "batctl -m vd json -n" into an array of dictionaries. +class Batman(object): """ - output = subprocess.check_output(["batctl","-m",self.mesh_interface,"vd","json","-n"]) - lines = output.splitlines() - vds = self.vis_data_helper(lines) - return vds - - def vis_data_batadv_vis(self): - """ Parse "batadv-vis -i -f json" into an array of dictionaries. + Bindings for B.A.T.M.A.N. Advanced + commandline interface "batctl" """ - output = subprocess.check_output(["batadv-vis","-i",self.mesh_interface,"-f","json"]) - lines = output.splitlines() - return self.vis_data_helper(lines) + def __init__(self, mesh_interface='bat0'): + self.mesh_interface = mesh_interface - def gateway_list(self): - """ Parse "batctl -m gwl -n" into an array of dictionaries. - """ - output = subprocess.check_output(["batctl","-m",self.mesh_interface,"gwl","-n"]) - output_utf8 = output.decode("utf-8") - lines = output_utf8.splitlines() + def vis_data(self, batadv_vis=False): + vds = self.vis_data_batctl_legacy() + if batadv_vis: + vds += self.vis_data_batadv_vis() + return vds - own_mac = re.match(r"^.*MainIF/MAC: [^/]+/([0-9a-f:]+).*$", lines[0]).group(1) + @staticmethod + def vis_data_helper(lines): + vd_tmp = [] + for line in lines: + try: + utf8_line = line.decode('utf-8') + vd_tmp.append(json.loads(utf8_line)) + except UnicodeDecodeError: + pass + return vd_tmp - gw = [] - gw_mode = self.gateway_mode() - if gw_mode['mode'] == 'server': - gw.append(own_mac) + def vis_data_batctl_legacy(self): + """ + Parse "batctl -m vd json -n" into an array of dictionaries. + """ + output = subprocess.check_output(['batctl', '-m', self.mesh_interface, 'vd', 'json', '-n']) + lines = output.splitlines() + vds = self.vis_data_helper(lines) + return vds - for line in lines: - gw_line = re.match(r"^(?:=>)? +([0-9a-f:]+) ", line) - if gw_line: - gw.append(gw_line.group(1)) + def vis_data_batadv_vis(self): + """ + Parse "batadv-vis -i -f json" into an array of dictionaries. + """ + output = subprocess.check_output(['batadv-vis', '-i', self.mesh_interface, '-f', 'json']) + lines = output.splitlines() + return self.vis_data_helper(lines) - return gw + def gateway_list(self): + """ + Parse "batctl -m gwl -n" into an array of dictionaries. + """ + output = subprocess.check_output(['batctl', '-m', self.mesh_interface, 'gwl', '-n']) + output_utf8 = output.decode('utf-8') + lines = output_utf8.splitlines() - def gateway_mode(self): - """ Parse "batctl -m gw" - """ - output = subprocess.check_output(["batctl","-m",self.mesh_interface,"gw"]) - elements = output.decode("utf-8").split() - mode = elements[0] - if mode == "server": - return {'mode': 'server', 'bandwidth': elements[3]} - else: - return {'mode': mode} + own_mac = re.match(r"^.*MainIF/MAC: [^/]+/([0-9a-f:]+).*$", lines[0]).group(1) + + gateways = [] + gw_mode = self.gateway_mode() + if gw_mode['mode'] == 'server': + gateways.append(own_mac) + + for line in lines: + gw_line = re.match(r"^(?:=>)? +([0-9a-f:]+) ", line) + if gw_line: + gateways.append(gw_line.group(1)) + + return gateways + + def gateway_mode(self): + """ + Parse "batctl -m gw" + """ + output = subprocess.check_output(['batctl', '-m', self.mesh_interface, 'gw']) + elements = output.decode("utf-8").split() + mode = elements[0] + if mode == 'server': + return {'mode': 'server', + 'bandwidth': elements[3]} + else: + return {'mode': mode} if __name__ == "__main__": - bc = batman() - vd = bc.vis_data() - gw = bc.gateway_list() - for x in vd: - print(x) - print(gw) - print(bc.gateway_mode()) + bc = Batman() + vd = bc.vis_data() + gw = bc.gateway_list() + for x in vd: + print(x) + print(gw) + print(bc.gateway_mode()) diff --git a/graph.py b/graph.py index b6e86aa..460e327 100644 --- a/graph.py +++ b/graph.py @@ -1,66 +1,74 @@ import networkx as nx -from copy import deepcopy from functools import reduce from itertools import chain from nodes import build_mac_table -def import_vis_data(graph, nodes, vis_data): - macs = build_mac_table(nodes) - nodes_a = map(lambda d: 2*[d['primary']], filter(lambda d: 'primary' in d, vis_data)) - nodes_b = map(lambda d: [d['secondary'], d['of']], filter(lambda d: 'secondary' in d, vis_data)) - graph.add_nodes_from(map(lambda a, b: (a, dict(primary=b, node_id=macs.get(b))), *zip(*chain(nodes_a, nodes_b)))) - edges = filter(lambda d: 'neighbor' in d, vis_data) - graph.add_edges_from(map(lambda d: (d['router'], d['neighbor'], dict(tq=float(d['label']))), edges)) +def import_vis_data(graph, nodes, vis_data): + macs = build_mac_table(nodes) + nodes_a = map(lambda d: 2*[d['primary']], filter(lambda d: 'primary' in d, vis_data)) + nodes_b = map(lambda d: [d['secondary'], d['of']], filter(lambda d: 'secondary' in d, vis_data)) + graph.add_nodes_from(map(lambda a, b: (a, dict(primary=b, node_id=macs.get(b))), *zip(*chain(nodes_a, nodes_b)))) + + edges = filter(lambda d: 'neighbor' in d, vis_data) + graph.add_edges_from(map(lambda d: (d['router'], d['neighbor'], dict(tq=float(d['label']))), edges)) + def mark_vpn(graph, vpn_macs): - components = map(frozenset, nx.weakly_connected_components(graph)) - components = filter(vpn_macs.intersection, components) - nodes = reduce(lambda a, b: a | b, components, set()) + components = map(frozenset, nx.weakly_connected_components(graph)) + components = filter(vpn_macs.intersection, components) + nodes = reduce(lambda a, b: a | b, components, set()) + + for node in nodes: + for k, v in graph[node].items(): + v['vpn'] = True - for node in nodes: - for k, v in graph[node].items(): - v['vpn'] = True def to_multigraph(graph): - def f(a): - node = graph.node[a] - return node['primary'] if node else a + def f(a): + node = graph.node[a] + return node['primary'] if node else a - G = nx.MultiDiGraph() - map_node = lambda node, data: (data['primary'], dict(node_id=data['node_id'])) if data else (node, dict()) - G.add_nodes_from(map(map_node, *zip(*graph.nodes_iter(data=True)))) - G.add_edges_from(map(lambda a, b, data: (f(a), f(b), data), *zip(*graph.edges_iter(data=True)))) + def map_node(node, data): + return (data['primary'], dict(node_id=data['node_id'])) if data else (node, dict()) + + digraph = nx.MultiDiGraph() + digraph.add_nodes_from(map(map_node, *zip(*graph.nodes_iter(data=True)))) + digraph.add_edges_from(map(lambda a, b, data: (f(a), f(b), data), *zip(*graph.edges_iter(data=True)))) + + return digraph - return G def merge_nodes(graph): - def merge_edges(data): - tq = min(map(lambda d: d['tq'], data)) - vpn = all(map(lambda d: d.get('vpn', False), data)) - return dict(tq=tq, vpn=vpn) + def merge_edges(data): + tq = min(map(lambda d: d['tq'], data)) + vpn = all(map(lambda d: d.get('vpn', False), data)) + return dict(tq=tq, vpn=vpn) - G = to_multigraph(graph) - H = nx.DiGraph() - H.add_nodes_from(G.nodes_iter(data=True)) - edges = chain.from_iterable([[(e, d, merge_edges(G[e][d].values())) for d in G[e]] for e in G]) - H.add_edges_from(edges) + multigraph = to_multigraph(graph) + digraph = nx.DiGraph() + digraph.add_nodes_from(multigraph.nodes_iter(data=True)) + edges = chain.from_iterable([[(e, d, merge_edges(multigraph[e][d].values())) + for d in multigraph[e]] for e in multigraph]) + digraph.add_edges_from(edges) + + return digraph - return H def to_undirected(graph): - G = nx.MultiGraph() - G.add_nodes_from(graph.nodes_iter(data=True)) - G.add_edges_from(graph.edges_iter(data=True)) + multigraph = nx.MultiGraph() + multigraph.add_nodes_from(graph.nodes_iter(data=True)) + multigraph.add_edges_from(graph.edges_iter(data=True)) - def merge_edges(data): - tq = max(map(lambda d: d['tq'], data)) - vpn = all(map(lambda d: d.get('vpn', False), data)) - return dict(tq=tq, vpn=vpn, bidirect=len(data) == 2) + def merge_edges(data): + tq = max(map(lambda d: d['tq'], data)) + vpn = all(map(lambda d: d.get('vpn', False), data)) + return dict(tq=tq, vpn=vpn, bidirect=len(data) == 2) - H = nx.Graph() - H.add_nodes_from(G.nodes_iter(data=True)) - edges = chain.from_iterable([[(e, d, merge_edges(G[e][d].values())) for d in G[e]] for e in G]) - H.add_edges_from(edges) + graph = nx.Graph() + graph.add_nodes_from(multigraph.nodes_iter(data=True)) + edges = chain.from_iterable([[(e, d, merge_edges(multigraph[e][d].values())) + for d in multigraph[e]] for e in multigraph]) + graph.add_edges_from(edges) - return H + return graph diff --git a/nodes.py b/nodes.py index 61949e1..23a2b0e 100644 --- a/nodes.py +++ b/nodes.py @@ -2,128 +2,137 @@ from collections import Counter, defaultdict from datetime import datetime from functools import reduce -def build_mac_table(nodes): - macs = dict() - for node_id, node in nodes.items(): - try: - for mac in node['nodeinfo']['network']['mesh_interfaces']: - macs[mac] = node_id - except KeyError: - pass - return macs +def build_mac_table(nodes): + macs = dict() + for node_id, node in nodes.items(): + try: + for mac in node['nodeinfo']['network']['mesh_interfaces']: + macs[mac] = node_id + except KeyError: + pass + return macs + def prune_nodes(nodes, now, days): - prune = [] - for node_id, node in nodes.items(): - if not 'lastseen' in node: - prune.append(node_id) - continue + prune = [] + for node_id, node in nodes.items(): + if 'lastseen' not in node: + prune.append(node_id) + continue - lastseen = datetime.strptime(node['lastseen'], '%Y-%m-%dT%H:%M:%S') - delta = (now - lastseen).seconds + lastseen = datetime.strptime(node['lastseen'], '%Y-%m-%dT%H:%M:%S') + delta = (now - lastseen).seconds - if delta >= days * 86400: - prune.append(node_id) + if delta >= days * 86400: + prune.append(node_id) + + for node_id in prune: + del nodes[node_id] - for node_id in prune: - del nodes[node_id] def mark_online(node, now): - node['lastseen'] = now.isoformat() - node.setdefault('firstseen', now.isoformat()) - node['flags']['online'] = True + node['lastseen'] = now.isoformat() + node.setdefault('firstseen', now.isoformat()) + node['flags']['online'] = True + def import_nodeinfo(nodes, nodeinfos, now, assume_online=False): - for nodeinfo in filter(lambda d: 'node_id' in d, nodeinfos): - node = nodes.setdefault(nodeinfo['node_id'], {'flags': dict()}) - node['nodeinfo'] = nodeinfo - node['flags']['online'] = False - node['flags']['gateway'] = False + for nodeinfo in filter(lambda d: 'node_id' in d, nodeinfos): + node = nodes.setdefault(nodeinfo['node_id'], {'flags': dict()}) + node['nodeinfo'] = nodeinfo + node['flags']['online'] = False + node['flags']['gateway'] = False + + if assume_online: + mark_online(node, now) - if assume_online: - mark_online(node, now) def reset_statistics(nodes): - for node in nodes.values(): - node['statistics'] = { 'clients': 0 } + for node in nodes.values(): + node['statistics'] = {'clients': 0} + def import_statistics(nodes, statistics): - def add(node, statistics, target, source, f=lambda d: d): - try: - node['statistics'][target] = f(reduce(dict.__getitem__, source, statistics)) - except (KeyError,TypeError): - pass + def add(node, statistics, target, source, f=lambda d: d): + try: + node['statistics'][target] = f(reduce(dict.__getitem__, source, statistics)) + except (KeyError, TypeError): + pass + + macs = build_mac_table(nodes) + statistics = filter(lambda d: 'node_id' in d, statistics) + statistics = filter(lambda d: d['node_id'] in nodes, statistics) + for node, statistics in map(lambda d: (nodes[d['node_id']], d), statistics): + add(node, statistics, 'clients', ['clients', 'total']) + add(node, statistics, 'gateway', ['gateway'], lambda d: macs.get(d, d)) + add(node, statistics, 'uptime', ['uptime']) + add(node, statistics, 'loadavg', ['loadavg']) + add(node, statistics, 'memory_usage', ['memory'], lambda d: 1 - d['free'] / d['total']) + add(node, statistics, 'rootfs_usage', ['rootfs_usage']) - macs = build_mac_table(nodes) - statistics = filter(lambda d: 'node_id' in d, statistics) - statistics = filter(lambda d: d['node_id'] in nodes, statistics) - for node, statistics in map(lambda d: (nodes[d['node_id']], d), statistics): - add(node, statistics, 'clients', ['clients', 'total']) - add(node, statistics, 'gateway', ['gateway'], lambda d: macs.get(d, d)) - add(node, statistics, 'uptime', ['uptime']) - add(node, statistics, 'loadavg', ['loadavg']) - add(node, statistics, 'memory_usage', ['memory'], lambda d: 1 - d['free'] / d['total']) - add(node, statistics, 'rootfs_usage', ['rootfs_usage']) def import_mesh_ifs_vis_data(nodes, vis_data): - macs = build_mac_table(nodes) + macs = build_mac_table(nodes) - mesh_ifs = defaultdict(lambda: set()) - for line in filter(lambda d: 'secondary' in d, vis_data): - primary = line['of'] - mesh_ifs[primary].add(primary) - mesh_ifs[primary].add(line['secondary']) + mesh_ifs = defaultdict(lambda: set()) + for line in filter(lambda d: 'secondary' in d, vis_data): + primary = line['of'] + mesh_ifs[primary].add(primary) + mesh_ifs[primary].add(line['secondary']) - def if_to_node(ifs): - a = filter(lambda d: d in macs, ifs) - a = map(lambda d: nodes[macs[d]], a) - try: - return (next(a), ifs) - except StopIteration: - return None + def if_to_node(ifs): + a = filter(lambda d: d in macs, ifs) + a = map(lambda d: nodes[macs[d]], a) + try: + return next(a), ifs + except StopIteration: + return None - mesh_nodes = filter(lambda d: d, map(if_to_node, mesh_ifs.values())) + mesh_nodes = filter(lambda d: d, map(if_to_node, mesh_ifs.values())) - for v in mesh_nodes: - node = v[0] + for v in mesh_nodes: + node = v[0] - try: - mesh_ifs = set(node['nodeinfo']['network']['mesh_interfaces']) - except KeyError: - mesh_ifs = set() + try: + mesh_ifs = set(node['nodeinfo']['network']['mesh_interfaces']) + except KeyError: + mesh_ifs = set() + + node['nodeinfo']['network']['mesh_interfaces'] = list(mesh_ifs | v[1]) - node['nodeinfo']['network']['mesh_interfaces'] = list(mesh_ifs | v[1]) def import_vis_clientcount(nodes, vis_data): - macs = build_mac_table(nodes) - data = filter(lambda d: d.get('label', None) == 'TT', vis_data) - data = filter(lambda d: d['router'] in macs, data) - data = map(lambda d: macs[d['router']], data) + macs = build_mac_table(nodes) + data = filter(lambda d: d.get('label', None) == 'TT', vis_data) + data = filter(lambda d: d['router'] in macs, data) + data = map(lambda d: macs[d['router']], data) + + for node_id, clientcount in Counter(data).items(): + nodes[node_id]['statistics'].setdefault('clients', clientcount) - for node_id, clientcount in Counter(data).items(): - nodes[node_id]['statistics'].setdefault('clients', clientcount) def mark_gateways(nodes, gateways): - macs = build_mac_table(nodes) - gateways = filter(lambda d: d in macs, gateways) + macs = build_mac_table(nodes) + gateways = filter(lambda d: d in macs, gateways) + + for node in map(lambda d: nodes[macs[d]], gateways): + node['flags']['gateway'] = True - for node in map(lambda d: nodes[macs[d]], gateways): - node['flags']['gateway'] = True def mark_vis_data_online(nodes, vis_data, now): - macs = build_mac_table(nodes) + macs = build_mac_table(nodes) - online = set() - for line in vis_data: - if 'primary' in line: - online.add(line['primary']) - elif 'secondary' in line: - online.add(line['secondary']) - elif 'gateway' in line: - # This matches clients' MACs. - # On pre-Gluon nodes the primary MAC will be one of it. - online.add(line['gateway']) + online = set() + for line in vis_data: + if 'primary' in line: + online.add(line['primary']) + elif 'secondary' in line: + online.add(line['secondary']) + elif 'gateway' in line: + # This matches clients' MACs. + # On pre-Gluon nodes the primary MAC will be one of it. + online.add(line['gateway']) - for mac in filter(lambda d: d in macs, online): - mark_online(nodes[macs[mac]], now) + for mac in filter(lambda d: d in macs, online): + mark_online(nodes[macs[mac]], now) diff --git a/rrddb.py b/rrddb.py index 2fccff4..b023e6b 100644 --- a/rrddb.py +++ b/rrddb.py @@ -1,50 +1,50 @@ #!/usr/bin/env python3 -import subprocess import time import os from GlobalRRD import GlobalRRD from NodeRRD import NodeRRD -class rrd: - def __init__( self - , databaseDirectory - , imagePath - , displayTimeGlobal = "7d" - , displayTimeNode = "1d" - ): - self.dbPath = databaseDirectory - self.globalDb = GlobalRRD(self.dbPath) - self.imagePath = imagePath - self.displayTimeGlobal = displayTimeGlobal - self.displayTimeNode = displayTimeNode - self.currentTimeInt = (int(time.time())/60)*60 - self.currentTime = str(self.currentTimeInt) +class RRD(object): + def __init__(self, + database_directory, + image_path, + display_time_global="7d", + display_time_node="1d"): - try: - os.stat(self.imagePath) - except: - os.mkdir(self.imagePath) + self.dbPath = database_directory + self.globalDb = GlobalRRD(self.dbPath) + self.imagePath = image_path + self.displayTimeGlobal = display_time_global + self.displayTimeNode = display_time_node - def update_database(self, nodes): - online_nodes = dict(filter(lambda d: d[1]['flags']['online'], nodes.items())) - client_count = sum(map(lambda d: d['statistics']['clients'], online_nodes.values())) + self.currentTimeInt = (int(time.time())/60)*60 + self.currentTime = str(self.currentTimeInt) - self.globalDb.update(len(online_nodes), client_count) - for node_id, node in online_nodes.items(): - rrd = NodeRRD(os.path.join(self.dbPath, node_id + '.rrd'), node) - rrd.update() + try: + os.stat(self.imagePath) + except OSError: + os.mkdir(self.imagePath) - def update_images(self): - self.globalDb.graph(os.path.join(self.imagePath, "globalGraph.png"), self.displayTimeGlobal) + def update_database(self, nodes): + online_nodes = dict(filter(lambda d: d[1]['flags']['online'], nodes.items())) + client_count = sum(map(lambda d: d['statistics']['clients'], online_nodes.values())) - nodeDbFiles = os.listdir(self.dbPath) + self.globalDb.update(len(online_nodes), client_count) + for node_id, node in online_nodes.items(): + rrd = NodeRRD(os.path.join(self.dbPath, node_id + '.rrd'), node) + rrd.update() - for fileName in nodeDbFiles: - if not os.path.isfile(os.path.join(self.dbPath, fileName)): - continue + def update_images(self): + self.globalDb.graph(os.path.join(self.imagePath, "globalGraph.png"), self.displayTimeGlobal) - nodeName = os.path.basename(fileName).split('.') - if nodeName[1] == 'rrd' and not nodeName[0] == "nodes": - rrd = NodeRRD(os.path.join(self.dbPath, fileName)) - rrd.graph(self.imagePath, self.displayTimeNode) + nodedb_files = os.listdir(self.dbPath) + + for file_name in nodedb_files: + if not os.path.isfile(os.path.join(self.dbPath, file_name)): + continue + + node_name = os.path.basename(file_name).split('.') + if node_name[1] == 'rrd' and not node_name[0] == "nodes": + rrd = NodeRRD(os.path.join(self.dbPath, file_name)) + rrd.graph(self.imagePath, self.displayTimeNode)