#!/usr/bin/env python # This file is part of dnmapper, an AS--level mapping tool # Licensed under GNU General Public License v3 or later # Written by Sebastian Lohff (seba@someserver.de) from __future__ import print_function from collections import OrderedDict import re import socket from backend.exceptions import RouterParserException 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 = list(filter(lambda _z: _z, map(lambda _y: list(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] == ['IPv4', 'Unicast', 'Summary:']: del(status[0]) 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 = list(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).items(): 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], "online": data[n][2] == "Established", "neighbor_address": data[n+1][2], "neighbor_as": int(data[n+2][2]), "neighbor_id": data[n+3][2] if len(data) > n+3 and data[n+3][0:2] == ["Neighbor", "ID:"] else None, "last_error": " ".join(data[n+3][2:]) if len(data) > n+3 and 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": list(map(int, candidate["path"])), "iBGP": candidate["iBGP"]} routes.append(route) 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 if raw[descrIdx + 1][0] == "Hostname:": descrIdx += 1 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(","), "online": raw[2+descrIdx][3].strip(",") == "Established", "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") or line.startswith("Displayed "): 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 = list(filter(lambda _x: _x not in ('', 'i'), line[61:].split(" "))) # currently skip incomplete routes if '?' not in path: route = {"prefix": d["network"], "nexthop": d["nexthop"], "path": list(map(int, path)), "iBGP": d["origin"] == "i"} routes.append(route) return routes