You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
354 lines
11 KiB
354 lines
11 KiB
#!/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<status>.)(?P<status2>.)(?P<origin>.)(?P<network>[0-9./]+)?\s+(?P<nexthop>[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
|
|
|