commit 12dfa7f3b4a58a0a3fb60ff51c7a965ba17aea28 Author: Sebastian Lohff Date: Mon Mar 23 01:42:31 2015 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..53ef0cd --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*.pyc +*.pyo +.*.swp +.*.swo +db.sqlite3 diff --git a/bgpdata/__init__.py b/bgpdata/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bgpdata/admin.py b/bgpdata/admin.py new file mode 100644 index 0000000..49a0d8f --- /dev/null +++ b/bgpdata/admin.py @@ -0,0 +1,12 @@ +from django.contrib import admin +from bgpdata.models import ConfigHost, CrawlRun, CrawlLog, AS, BorderRouter, Announcement, Peering, BorderRouterPair + +# Register your models here. +admin.site.register(ConfigHost) +admin.site.register(CrawlRun) +admin.site.register(CrawlLog) +admin.site.register(AS) +admin.site.register(BorderRouter) +admin.site.register(Announcement) +admin.site.register(Peering) +admin.site.register(BorderRouterPair) diff --git a/bgpdata/migrations/0001_initial.py b/bgpdata/migrations/0001_initial.py new file mode 100644 index 0000000..6948af1 --- /dev/null +++ b/bgpdata/migrations/0001_initial.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Announcement', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('ip', models.GenericIPAddressField()), + ('prefix', models.IntegerField()), + ('ASPath', models.CharField(max_length=512)), + ('nextHop', models.GenericIPAddressField()), + ], + options={ + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='AS', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('number', models.IntegerField()), + ], + options={ + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='BorderRouter', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('pingable', models.BooleanField(default=False)), + ('reachable', models.BooleanField(default=False)), + ('AS', models.ForeignKey(to='bgpdata.AS')), + ], + options={ + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='ConfigHost', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('number', models.IntegerField()), + ('ip', models.GenericIPAddressField()), + ('checkMethod', models.CharField(max_length=4, choices=[(b'CMK', b'Check MK'), (b'PLAIN', b'Plain')])), + ], + options={ + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='CrawlRun', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('startTime', models.DateTimeField()), + ('endTime', models.DateTimeField(blank=True)), + ], + options={ + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='Peering', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('origin', models.CharField(max_length=10, choices=[(b'path', b'BGP Path'), (b'direct', b'Direct Connection')])), + ('as1', models.ForeignKey(related_name='peering1', to='bgpdata.AS')), + ('as2', models.ForeignKey(related_name='peering2', to='bgpdata.AS')), + ('router1', models.ForeignKey(related_name='peering1', default=None, to='bgpdata.BorderRouter', null=True)), + ('router2', models.ForeignKey(related_name='peering2', default=None, to='bgpdata.BorderRouter', null=True)), + ], + options={ + }, + bases=(models.Model,), + ), + migrations.AddField( + model_name='as', + name='crawl', + field=models.ForeignKey(to='bgpdata.CrawlRun'), + preserve_default=True, + ), + migrations.AddField( + model_name='announcement', + name='originAS', + field=models.ForeignKey(to='bgpdata.AS'), + preserve_default=True, + ), + ] diff --git a/bgpdata/migrations/0002_confighost_name.py b/bgpdata/migrations/0002_confighost_name.py new file mode 100644 index 0000000..4da9b6a --- /dev/null +++ b/bgpdata/migrations/0002_confighost_name.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('bgpdata', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='confighost', + name='name', + field=models.CharField(default='', max_length=50), + preserve_default=False, + ), + ] diff --git a/bgpdata/migrations/0003_crawllog.py b/bgpdata/migrations/0003_crawllog.py new file mode 100644 index 0000000..2c39a81 --- /dev/null +++ b/bgpdata/migrations/0003_crawllog.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('bgpdata', '0002_confighost_name'), + ] + + operations = [ + migrations.CreateModel( + name='CrawlLog', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('logtime', models.DateTimeField(auto_now_add=True)), + ('severity', models.CharField(max_length=10, choices=[(b'INFO', b'info'), (b'ERROR', b'error'), (b'DEBUG', b'debug'), (b'WARN', b'warning')])), + ('message', models.TextField()), + ('crawl', models.ForeignKey(to='bgpdata.CrawlRun')), + ('host', models.ForeignKey(to='bgpdata.ConfigHost')), + ], + options={ + }, + bases=(models.Model,), + ), + ] diff --git a/bgpdata/migrations/0004_auto_20150321_1607.py b/bgpdata/migrations/0004_auto_20150321_1607.py new file mode 100644 index 0000000..94f9863 --- /dev/null +++ b/bgpdata/migrations/0004_auto_20150321_1607.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('bgpdata', '0003_crawllog'), + ] + + operations = [ + migrations.AlterField( + model_name='crawlrun', + name='endTime', + field=models.DateTimeField(null=True, blank=True), + preserve_default=True, + ), + ] diff --git a/bgpdata/migrations/0005_auto_20150321_1608.py b/bgpdata/migrations/0005_auto_20150321_1608.py new file mode 100644 index 0000000..11e5325 --- /dev/null +++ b/bgpdata/migrations/0005_auto_20150321_1608.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('bgpdata', '0004_auto_20150321_1607'), + ] + + operations = [ + migrations.AlterField( + model_name='crawllog', + name='host', + field=models.ForeignKey(to='bgpdata.ConfigHost', null=True), + preserve_default=True, + ), + ] diff --git a/bgpdata/migrations/0006_auto_20150321_1845.py b/bgpdata/migrations/0006_auto_20150321_1845.py new file mode 100644 index 0000000..bdd127d --- /dev/null +++ b/bgpdata/migrations/0006_auto_20150321_1845.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('bgpdata', '0005_auto_20150321_1608'), + ] + + operations = [ + migrations.AddField( + model_name='borderrouter', + name='routerID', + field=models.GenericIPAddressField(default=None), + preserve_default=False, + ), + migrations.AlterField( + model_name='crawllog', + name='host', + field=models.ForeignKey(blank=True, to='bgpdata.ConfigHost', null=True), + preserve_default=True, + ), + ] diff --git a/bgpdata/migrations/0007_auto_20150322_1828.py b/bgpdata/migrations/0007_auto_20150322_1828.py new file mode 100644 index 0000000..8577d04 --- /dev/null +++ b/bgpdata/migrations/0007_auto_20150322_1828.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('bgpdata', '0006_auto_20150321_1845'), + ] + + operations = [ + migrations.CreateModel( + name='BorderRouterPair', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('router1', models.ForeignKey(related_name='routerpair1', default=None, blank=True, to='bgpdata.BorderRouter', null=True)), + ('router2', models.ForeignKey(related_name='routerpair2', default=None, blank=True, to='bgpdata.BorderRouter', null=True)), + ], + options={ + }, + bases=(models.Model,), + ), + migrations.RemoveField( + model_name='peering', + name='router1', + ), + migrations.RemoveField( + model_name='peering', + name='router2', + ), + migrations.AddField( + model_name='announcement', + name='router', + field=models.ForeignKey(default=None, to='bgpdata.BorderRouter'), + preserve_default=False, + ), + migrations.AddField( + model_name='peering', + name='routers', + field=models.ManyToManyField(default=None, to='bgpdata.BorderRouterPair', null=True, blank=True), + preserve_default=True, + ), + ] diff --git a/bgpdata/migrations/0008_auto_20150322_1906.py b/bgpdata/migrations/0008_auto_20150322_1906.py new file mode 100644 index 0000000..d6ae2f3 --- /dev/null +++ b/bgpdata/migrations/0008_auto_20150322_1906.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('bgpdata', '0007_auto_20150322_1828'), + ] + + operations = [ + migrations.RemoveField( + model_name='peering', + name='routers', + ), + migrations.AddField( + model_name='borderrouterpair', + name='peering', + field=models.ForeignKey(default=None, to='bgpdata.Peering'), + preserve_default=False, + ), + ] diff --git a/bgpdata/migrations/__init__.py b/bgpdata/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bgpdata/models.py b/bgpdata/models.py new file mode 100644 index 0000000..f0ebee6 --- /dev/null +++ b/bgpdata/models.py @@ -0,0 +1,142 @@ +from django.db import models +from django.db.models import Q + + +# Create your models here. +class ConfigHost(models.Model): + CHECK_CHOICES = ( + ('CMK', "Check MK"), + ('PLAIN', "Plain"), + ) + + # asno, ip, check method, + name = models.CharField(max_length=50) + number = models.IntegerField() + ip = models.GenericIPAddressField() + checkMethod = models.CharField(max_length=4, choices=CHECK_CHOICES) + + def __unicode__(self): + return u"%s (%s / %s)" % (self.name, self.number, self.ip) + +class CrawlRun(models.Model): + # time start, time end, + startTime = models.DateTimeField() + endTime = models.DateTimeField(null=True, blank=True) + + def __unicode__(self): + return u"Run %d - %s to %s" % (self.pk, self.startTime, self.endTime if self.endTime else "?") + +class CrawlLog(models.Model): + INFO = 'INFO' + ERROR = 'ERROR' + DEBUG = 'DEBUG' + WARN = 'WARN' + SEVERITY = ( + (INFO, 'info'), + (ERROR, 'error'), + (DEBUG, 'debug'), + (WARN, 'warning'), + ) + + crawl = models.ForeignKey(CrawlRun) + host = models.ForeignKey(ConfigHost, null=True, blank=True) + logtime = models.DateTimeField(auto_now_add=True) + severity = models.CharField(max_length=10, choices=SEVERITY) + message = models.TextField() + + @staticmethod + def log(crawl, msg, severity=None, host=None): + if not severity: + severity = CrawlLog.ERROR + + log = CrawlLog() + log.crawl = crawl + log.message = msg + log.severity = severity + log.host = host + log.save() + + def __unicode__(self): + host = "host %s - " % self.host.name if self.host else "" + return u"Log %s %s: %s%s" % (self.get_severity_display(), self.logtime, host, self.message) + +class AS(models.Model): + # asno + crawl = models.ForeignKey(CrawlRun) + number = models.IntegerField() + + def __unicode__(self): + return u"AS %s (crawl %d)" % (self.number, self.crawl.pk) + + def getPeerings(self): + return Peering.objects.filter(Q(as1=self)|Q(as2=self)) + +class BorderRouter(models.Model): + # as id, ip, check method, pingable, reachable + # unique: (crawl_id, asno, as id) + AS = models.ForeignKey(AS) + routerID = models.GenericIPAddressField() + + pingable = models.BooleanField(default=False) + reachable = models.BooleanField(default=False) + + def __unicode__(self): + p = "p" if self.pingable else "!p" + r = "r" if self.reachable else "!r" + return u"Router %s (AS %s, %s%s)" % (self.routerID, self.AS.number, p, r) + +class Announcement(models.Model): + router = models.ForeignKey(BorderRouter) + + ip = models.GenericIPAddressField() + prefix = models.IntegerField() + + # NOTE: increase length for longer pathes (currently supports a length of ~85) + ASPath = models.CharField(max_length=512) + nextHop = models.GenericIPAddressField() + originAS = models.ForeignKey(AS) + + def __unicode__(self): + return u"%s/%s via %s (crawl %s)" % (self.ip, self.prefix, self.ASPath, self.router.AS.crawl.pk) + +class Peering(models.Model): + DIRECT = 'direct' + PATH = 'path' + + ORIGIN = ( + (PATH, 'BGP Path'), + (DIRECT, 'Direct Connection'), + ) + + as1 = models.ForeignKey(AS, related_name='peering1') + as2 = models.ForeignKey(AS, related_name='peering2') + origin = models.CharField(max_length=10, choices=ORIGIN) + + def __unicode__(self): + return u"AS %s <--> AS %s (%s, crawl %s)" % (self.as1.number, self.as2.number, self.get_origin_display(), self.as1.crawl.pk) + + def containsAS(self, AS): + return AS in (self.as1, self.as2) + + @staticmethod + def getPeering(as1, as2): + """ Find matching peering """ + try: + return Peering.objects.get(as1=as1, as2=as2) + except Peering.DoesNotExist: + return Peering.objects.get(as1=as2, as2=as1) + +class BorderRouterPair(models.Model): + peering = models.ForeignKey(Peering) + router1 = models.ForeignKey(BorderRouter, default=None, blank=True, null=True, related_name='routerpair1') + router2 = models.ForeignKey(BorderRouter, default=None, blank=True, null=True, related_name='routerpair2') + + def __unicode__(self): + return u"%s <--> %s (crawl %d)" % (self.router1, self.router2, self.router1.AS.crawl.pk) + + @staticmethod + def getPairing(peering, router1, router2): + try: + return BorderRouterPair.objects.get(peering=peering, router1=router1, router2=router2) + except BorderRouterPair.DoesNotExist: + return BorderRouterPair.objects.get(peering=peering, router1=router2, router2=router1) diff --git a/bgpdata/tests.py b/bgpdata/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/bgpdata/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/bgpdata/views.py b/bgpdata/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/bgpdata/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/bin/crawl.py b/bin/crawl.py new file mode 100755 index 0000000..cfd68f6 --- /dev/null +++ b/bin/crawl.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python2 +from __future__ import print_function + +# prepare environment +import sys +sys.path.append("..") +import os +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dnmapper.settings") +import django +django.setup() + +from django.utils import timezone +from django.db.models import Q + +from bgpdata.models import ConfigHost, CrawlRun, CrawlLog, AS, BorderRouter, Announcement, Peering, BorderRouterPair +from routerparsers import getBGPData, RouterParserException + + +def getOrCreateAS(crawl, number): + currAS = None + try: + currAS = AS.objects.get(crawl=crawl, number=number) + except AS.DoesNotExist: + currAS = AS(crawl=crawl, number=number) + currAS.save() + + return currAS + +def main(): + # 1. create crawl run + crawl = CrawlRun() + crawl.startTime = timezone.now() + crawl.save() + + CrawlLog.log(crawl, "Starting crawl run!", severity=CrawlLog.INFO) + + # 2. get data from all hosts, put it in the database + for host in ConfigHost.objects.all(): + data = None + print(" -- Getting data for host %s" % host) + try: + if host.checkMethod == 'CMK': + data = getBGPData(host.ip, host.number) + else: + CrawlLog.log(crawl, "Method %s is not currently supported, skipping host" % host.checkMethod, host=host, severity=CrawlLog.ERROR) + continue + except RouterParserException as e: + msg = "Could not parse data for host: %s" % str(e) + print("%s: %s" % (host, msg)) + CrawlLog.log(crawl, msg, host=host, severity=CrawlLog.ERROR) + continue + + print(" -- parsing...") + + currASno = int(data["local_as"]) + currAS = getOrCreateAS(crawl, currASno) + + currRouter = None + try: + currRouter = BorderRouter.objects.get(AS=currAS, routerID=data["local_id"]) + currRouter.pingable = True + currRouter.reachable = True + currRouter.save() + except BorderRouter.DoesNotExist: + currRouter = BorderRouter(AS=currAS, routerID=data["local_id"], pingable=True, reachable=True) + currRouter.save() + + print(" --> peers") + for peer in data["peers"]: + # peerings + # data: BGP{state, neighbor_id, neighbor_as}, description + + # a) find/create neighbor + print(" ----> Peer:", int(peer["BGP"]["neighbor_as"])) + neighAS = None + try: + neighAS = AS.objects.get(crawl=crawl, number=int(peer["BGP"]["neighbor_as"])) + except AS.DoesNotExist: + neighAS = AS(crawl=crawl, number=int(peer["BGP"]["neighbor_as"])) + neighAS.save() + + # b) find out if a peering already exists (maybe where we only need to add our router id?) + peering = None + try: + peering = Peering.getPeering(currAS, neighAS) + except Peering.DoesNotExist: + peering = Peering(as1=currAS, as2=neighAS, origin=Peering.DIRECT) + peering.save() + + # c) look for router/peering pairs + if peer["BGP"]["neighbor_id"]: + try: + neighRouter = BorderRouter.objects.get(AS=neighAS, routerID=peer["BGP"]["neighbor_id"]) + except BorderRouter.DoesNotExist: + neighRouter = BorderRouter(AS=currAS, routerID=peer["BGP"]["neighbor_id"], pingable=False, reachable=False) + neighRouter.save() + try: + BorderRouterPair.getPairing(peering, currRouter, neighRouter) + except BorderRouterPair.DoesNotExist: + pairs = BorderRouterPair.objects.filter(Q(peering=peering) & (Q(router1=neighRouter, router2=None)|Q(router1=None, router2=neighRouter))) + if pairs.count() > 0: + pair = pairs[0] + if pair.router1 == None: + pair.router1 = currRouter + else: + pair.router2 = currRouter + pair.save() + else: + pair = BorderRouterPair(peering=peering, router1=currRouter, router2=neighRouter) + pair.save() + + print(" --> Announcements") + if "routes" in data and data["routes"]: + for route in data["routes"]: + print(" ---->", route["prefix"]) + if "/" not in route["prefix"]: + continue + ip, prefix = route["prefix"].split("/") + a = Announcement(router=currRouter, ip=ip, prefix=prefix, + ASPath=" ".join(route["path"]), nextHop=route["nexthop"], + originAS=currAS) + a.save() + else: + print(" !! No routes found in host output") + CrawlLog.log(crawl, "No routes found in host output (no bgp feed included?)", host=host, severity=CrawlLog.WARN) + + # 3. calculate missing data + print(" -- Adding extra data from announcements...") + # 3.1. use announcement data to find hidden peerings + for announcement in Announcement.objects.filter(router__AS__crawl=crawl): + path = announcement.ASPath.split(" ") + if len(path) > 1: + firstASno = path.pop(0) + firstAS = getOrCreateAS(crawl, firstASno) + while len(path) > 0: + secondASno = path.pop(0) + secondAS = getOrCreateAS(crawl, secondASno) + + try: + Peering.getPeering(firstAS, secondAS) + except Peering.DoesNotExist: + peering = Peering(as1=firstAS, as2=secondAS, origin=Peering.PATH) + peering.save() + + firstAS = secondAS + + # 4. end crawl run + crawl.endTime = timezone.now() + crawl.save() + + print(" !! Done") + CrawlLog.log(crawl, "Crawl completed", severity=CrawlLog.INFO) + +if __name__ == '__main__': + main() diff --git a/bin/routerparsers.py b/bin/routerparsers.py new file mode 100644 index 0000000..096d37a --- /dev/null +++ b/bin/routerparsers.py @@ -0,0 +1,340 @@ +#!/usr/bin/env python + +from __future__ import print_function + +import re +import socket + +from collections import OrderedDict + +class RouterParserException(Exception): + pass + +def err(msg): + raise RouterParserException(msg) + + +def getBGPData(ip, asno): + rawData = getDataFromHost(ip) + if not rawData: + err("Could not get data from host (empty response)") + + router = parseBGPData(rawData, asno) + + router["ip"] = ip + + return router + +def getDataFromHost(ip): + socket.setdefaulttimeout(5) + x = socket.socket() + x.connect((ip, 6556)) + f = x.makefile() + data = f.read() + x.close() + + return data + +def parseBGPData(raw, asno): + d = re.search(r"(?:^|\n)<<<(quagga|bird)>>>\n(.*?)(?:$|<<<[^\n]+>>>)", raw, re.DOTALL) + + if not d: + err("Data not found in check mk output") + + # mkify + raw = d.group(2).split("\n") + arr = filter(lambda _z: _z, map(lambda _y: filter(lambda _x: _x, re.split(r"\s+", _y)), raw)) + + # parse for bird/quagga + result = None + + if d.group(1) == "quagga": + result = parseQuagga(arr, raw, asno) + else: + result = parseBird(arr, raw, asno) + + return result + + +def parseQuagga(data, raw, asno): + status = _quaggaFindCommand(data, "show ip bgp sum") + if status[0][0:3] != ['BGP', 'router', 'identifier']: + print(status) + err("Couldn't find router id in quagga output") + + peers = _quaggaFindNeighbors(data) + if asno and int(asno) != int(status[0][7]): + err("AS number (%s) does not match as number from quagga (%s)" % (asno, status[0][7])) + + routes = _quaggaFindRoutes(raw) + + return {"local_id": status[0][3].strip(","), "local_as": int(status[0][7]), "peers": peers, "routes": routes} + +def parseBird(data, raw, asno): + status = _birdFindTable(data, "show status") + if status[2][0] != "1011-Router": + err("Couldn't find router id in bird output") + peers = filter(lambda _x: _x["type"] == "BGP", _birdMakeProtocols(data)) + + if asno == None: + err("Host is bird") + # FIXME + + routes = _birdFindRoutes(data) + + return {"local_id": status[2][3], "local_as": int(asno), "peers": peers, "routes": routes} + +def _birdFindTable(info, command): + """ find command output of a bird command, e.g. "show bgp neighbors" """ + command = ["bird>"] + command.split(" ") + commandInfo = [] + editNextLine = False + for line in info: + if not commandInfo: + if line == command: + commandInfo.append(line) + editNextLine = True + else: + if editNextLine: + editNextLine = False + commandInfo.append(line[1:]) + elif line[0] == "bird>": + return commandInfo + else: + commandInfo.append(line) + return [] + +def _birdFindProtocols(info): + """ return a list of tuples (protoname, protoinfo) """ + protocolTable = _birdFindTable(info, "show protocols all") + protocols = OrderedDict() + currProto = None + for line in protocolTable[2:]: + if line[0][0:4] == "1002": + currProto = line[0][5:] + protocols[currProto] = [[currProto] + line[1:]] + elif currProto == None: + err("No proto selected, couldn't parse line:", line) + else: + protocols[currProto].append(line) + + return protocols + +def _birdMakeProtocols(info): + """ Parse birds show protocols all output """ + # proto: name, type, description, state (up/down?), up-since + # routes imported, exported, preferred + # also: routing stats ( + # bgp special stuff: state, neighbor (address, as, id) (id not available when down) + # state (established, active) + # if error, last error is avilable + protocols = [] + for proto, data in _birdFindProtocols(info).iteritems(): + protoInfo = { + "name": proto, + "type": data[0][1], + "table": data[0][2], + "state": data[0][3], + "last_change": data[0][4], + "info": " ".join(data[0][5:]), + "description": " ".join(data[1][2:]), + "routes": { + "imported": data[5][1], + "exported": data[5][3], + "preferred": data[5][5], + } + } + if protoInfo["type"] == "BGP": + found = False + for n, line in enumerate(data): + if line[0:2] == ["BGP", "state:"]: + found = True + protoInfo["BGP"] = { + "state": data[n][2], + "neighbor_address": data[n+1][2], + "neighbor_as": int(data[n+2][2]), + "neighbor_id": data[n+3][2] if data[n+3][0:2] == ["Neighbor", "ID:"] else None, + "last_error": " ".join(data[n+3][2:]) if data[n+3][0:2] == ["Last", "error:"] else None, + } + + if not found: + protoInfo["BGP"] = None + + protocols.append(protoInfo) + + return protocols + + +def _birdFindRoutes(info): + output = _birdFindTable(info, "show route all") + if len(output) < 1: + # no data found + return None + + def handleCandidate(routes, candidate): + if candidate: + # path, nexthop, network + for key in ["path", "nexthop", "network", "iBGP"]: + if key not in candidate: + return + route = {"prefix": candidate["network"], "nexthop": candidate["nexthop"], "path": candidate["path"], "iBGP": candidate["iBGP"]} + routes.append(route) + pass + + routes = [] + candidate = None + lastIP = None + for line in output: + if line[0].startswith("1007-"): + # new route! + handleCandidate(routes, candidate) + if line[0] != "1007-": + # line has a network, use it! + lastIP = line[0][5:] + candidate = {"network": lastIP, "iBGP": None} + + elif candidate is not None: + # search bgp attributes + if line[0] == "1012-": + pass + k, v = line[1], line[2:] + else: + k, v = line[0], line[1:] + + k = k.rstrip(":") + if k == "BGP.next_hop": + candidate["nexthop"] = v[0] + elif k == "BGP.as_path": + candidate["path"] = v + + + handleCandidate(routes, candidate) + + return routes + + +def _quaggaFindCommand(info, cmd): + # ['core-frunde#', 'show', 'ip', 'bgp', 'sum'] + # ['core-frunde#', 'show', 'ip', 'bgp', 'neighbors'] + output = [] + cmd = cmd.split(" ") + prompt = None + for line in info: + if line[1:] == cmd: + prompt = line[0] + elif line[0] == prompt: + # done + return output + elif prompt != None: + output.append(line) + + err("Could not find command '%s' in output" % " ".join(cmd)) + +def _quaggaFindNeighbors(info): + #['BGP', 'neighbor', 'is', '10.50.1.2,', 'remote', 'AS', '65001,', 'local', 'AS', '65001,', 'internal', 'link'] + output = _quaggaFindCommand(info, "show ip bgp neighbors") + start = ["BGP", "neighbor", "is"] + + curr = None + rawNeighbors = [] + for line in output: + if line[0:3] == start: + if curr: + rawNeighbors.append(curr) + curr = [line] + elif curr: + curr.append(line) + else: + err("Could not find start of neighbors") + + if curr: + rawNeighbors.append(curr) + curr = None + + neighbors = [] + neighborDict = OrderedDict() + for raw in rawNeighbors: + descrIdx = 1 if raw[1][0] == "Description:" else 0 + peerdict = { + "neighbor_address": raw[0][3].rstrip(","), + "neighbor_as": int(raw[0][6].rstrip(",")), + "local_as": int(raw[0][9].rstrip(",")), + "description": " ".join(raw[1][1:]) if descrIdx else "No description", + "neighbor_id": raw[1+descrIdx][6].strip(","), + "state": raw[2+descrIdx][3].strip(","), + "routes": { + "imported": 0, + }, + "BGP": { + "state": raw[2+descrIdx][3].strip(","), + "neighbor_id": raw[1+descrIdx][6].strip(","), + "neighbor_address": raw[0][3].rstrip(","), + "neighbor_as": int(raw[0][6].rstrip(",")), + "state": raw[2+descrIdx][3].strip(","), + }, + } + + for line in raw: + if line[1:3] == ["accepted", "prefixes"]: + # woooo + peerdict["routes"]["imported"] = int(line[0]) + break + + neighbors.append(peerdict) + neighborDict[peerdict["neighbor_address"]] = peerdict + + return neighbors + +def _quaggaFindRoutes(raw): + # from # show ip bgp to Total number of prefixes XX + # BGP table version is 0, local router ID is 10.50.0.1 + # *> 10.3.14.0/27 10.75.0.22 0 65002 65112 i + cmdre = re.compile(r"^([^\s#]+#) show ip bgp$") + routere = re.compile(r"^(?P.)(?P.)(?P.)(?P[0-9./]+)?\s+(?P[0-9./]+)[\s0-9i]+$") + + # find output + output = [] + prompt = None + for line in raw: + if not prompt: + m = cmdre.match(line) + if m: + prompt = m.group(1) + " " + else: + if line.startswith(prompt): + break + else: + output.append(line) + + if len(output) < 1: + # no data found + return None + + routes = [] + foundTable = False + lastIP = None + for line in output: + if not foundTable: + if line.endswith("Metric LocPrf Weight Path"): + foundTable = True + else: + if line != '': + if line.startswith("Total number of prefixes"): + break + else: + # parse one route line + #print(line) + m = routere.match(line) + d = m.groupdict() + if d["network"]: + lastIP = d["network"] + else: + d["network"] = lastIP + + # "parse" path (everything after 61 chars, but no i) + path = filter(lambda _x: _x not in ('', 'i'), line[61:].split(" ")) + + route = {"prefix": d["network"], "nexthop": d["nexthop"], "path": path, "iBGP": d["origin"] == "i"} + routes.append(route) + + return routes diff --git a/dnmapper/__init__.py b/dnmapper/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dnmapper/settings.py b/dnmapper/settings.py new file mode 100644 index 0000000..1cf0b41 --- /dev/null +++ b/dnmapper/settings.py @@ -0,0 +1,84 @@ +""" +Django settings for dnmapper project. + +For more information on this file, see +https://docs.djangoproject.com/en/1.7/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/1.7/ref/settings/ +""" + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +import os +BASE_DIR = os.path.dirname(os.path.dirname(__file__)) + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'crv*hfx2pkxvq1s!)dbz*hdu+r7u2$y4djf6_#6mm)shk9e!58' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +TEMPLATE_DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = ( + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'bgpdata', +) + +MIDDLEWARE_CLASSES = ( + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +) + +ROOT_URLCONF = 'dnmapper.urls' + +WSGI_APPLICATION = 'dnmapper.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/1.7/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + } +} + +# Internationalization +# https://docs.djangoproject.com/en/1.7/topics/i18n/ + +LANGUAGE_CODE = 'de-de' + +TIME_ZONE = 'CET' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/1.7/howto/static-files/ + +STATIC_URL = '/static/' diff --git a/dnmapper/urls.py b/dnmapper/urls.py new file mode 100644 index 0000000..aff0b36 --- /dev/null +++ b/dnmapper/urls.py @@ -0,0 +1,10 @@ +from django.conf.urls import patterns, include, url +from django.contrib import admin + +urlpatterns = patterns('', + # Examples: + # url(r'^$', 'dnmapper.views.home', name='home'), + # url(r'^blog/', include('blog.urls')), + + url(r'^admin/', include(admin.site.urls)), +) diff --git a/dnmapper/wsgi.py b/dnmapper/wsgi.py new file mode 100644 index 0000000..9be8c96 --- /dev/null +++ b/dnmapper/wsgi.py @@ -0,0 +1,14 @@ +""" +WSGI config for dnmapper project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/ +""" + +import os +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dnmapper.settings") + +from django.core.wsgi import get_wsgi_application +application = get_wsgi_application() diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..71ce665 --- /dev/null +++ b/manage.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dnmapper.settings") + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv)