First Upload
This commit is contained in:
63
README.md
Normal file
63
README.md
Normal file
@@ -0,0 +1,63 @@
|
||||
BGP Dashboard
|
||||
=============
|
||||
|
||||
A "realtime" web view of your BGP network
|
||||
|
||||
- Who do I peer with?
|
||||
- How many routes do I receive from my peers?
|
||||
- Who do I use for tranist?
|
||||
- What AS path does a prefix take out of my network?
|
||||
- How many routes and autonomous systems do I see?
|
||||
- BGP Looking Glass (IPv4/IPv6/ASN)
|
||||
|
||||
|
||||
How it works
|
||||
---------
|
||||
> This is beta code.
|
||||
- BGP peering session using GoBGP
|
||||
- GoBGP pipes BGP information into MongoDB
|
||||
- Flask App queries MongoDB to build website and JSON API
|
||||
|
||||
###### This project uses three Docker containers
|
||||
- GoBGP ([osrg/gobgp](https://hub.docker.com/r/osrg/gobgp/))
|
||||
- MongoDB ([mongo](https://hub.docker.com/_/mongo/))
|
||||
- Flask ([docker-flask](https://hub.docker.com/r/p0bailey/docker-flask/))
|
||||
|
||||
###### GoBGP
|
||||
The GoBGP container serves two functions:
|
||||
- Peer with the "real" network
|
||||
- Configure [gobgpd.conf](https://github.com/rhicks/bgp-dash/blob/master/gobgp/gobgpd.conf) to peer with the real network.
|
||||
- Only IPv4-Unicast and IPv6-Unicast supported at this time.
|
||||
- Pass BGP updates into BGP
|
||||
- The [gobgp_to_mongo.py](https://github.com/rhicks/bgp-dash/blob/master/gobgp_to_mongo.py) script pipes the JSON updates from GoBGP into the MongoDB container
|
||||
|
||||
###### MongoDB
|
||||
- Mongo receives JSON updates from the GoBGP container
|
||||
- The Flask App queries Mongo for relevant information
|
||||
|
||||
###### Flask
|
||||
- Flask presents a Dashboard for realtime BGP updates
|
||||
- A JSON API is used on the backend to support the frontend and display Looking Glass queries
|
||||
|
||||
|
||||
Screenshot
|
||||
---------
|
||||

|
||||
|
||||
|
||||
Install
|
||||
---------
|
||||
```
|
||||
$ git clone https://github.com/NetDevUG/bgp-dashboard.git
|
||||
$ cd bgp-dashboard
|
||||
$ # modify ./gobgp/gobgpd.conf to peer with your network
|
||||
$ # modify ./flask/app/constants.py globals to use your ASN and BGP communities
|
||||
$ docker compose build
|
||||
$ docker compose up (watch the log to verify BGP peeering is established)
|
||||
```
|
||||
|
||||
|
||||
Todo
|
||||
---------
|
||||
- Update gobgp
|
||||
- Update Python Dependencys
|
BIN
bgp-dashboard.png
Normal file
BIN
bgp-dashboard.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 147 KiB |
33
bgp_attributes.py
Normal file
33
bgp_attributes.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# BGP Attributes
|
||||
PREFIX = 0
|
||||
ORIGIN = 1
|
||||
AS_PATH = 2
|
||||
NEXT_HOP = 3
|
||||
MULTI_EXIT_DISC = 4
|
||||
LOCAL_PREF = 5
|
||||
ATOMIC_AGGREGATE = 6
|
||||
AGGREGATOR = 7
|
||||
COMMUNITY = 8
|
||||
ORIGINATOR_ID = 9
|
||||
CLUSTER_LIST = 10
|
||||
DPA = 11
|
||||
ADVERTISER = 12
|
||||
CLUSTER_ID = 13
|
||||
MP_REACH_NLRI = 14
|
||||
MP_UNREACH_NLRI = 15
|
||||
EXTENDED_COMMUNITIES = 16
|
||||
LARGE_COMMUNITIES = 32
|
||||
#
|
||||
WITHDRAWAL = 11
|
||||
AGE = 12
|
||||
#
|
||||
ORIGIN_CODE = {
|
||||
0: 'IGP',
|
||||
1: 'EGP',
|
||||
2: 'INCOMPLETE'
|
||||
}
|
||||
# BGP UPDATE CODES
|
||||
OPEN = 1
|
||||
UPDATE = 2
|
||||
NOTIFICATION = 3
|
||||
KEEPALIVE = 4
|
23
docker-compose.yml
Executable file
23
docker-compose.yml
Executable file
@@ -0,0 +1,23 @@
|
||||
services:
|
||||
gobgp:
|
||||
build: ./gobgp
|
||||
ports:
|
||||
- "172.16.1.66:179:179"
|
||||
volumes:
|
||||
- .:/var/tmp
|
||||
restart: always
|
||||
|
||||
mongodb:
|
||||
image: mongo:latest
|
||||
expose:
|
||||
- 27017
|
||||
volumes:
|
||||
- ./db:/data/db
|
||||
|
||||
flask:
|
||||
build: ./flask
|
||||
ports:
|
||||
- 80:80
|
||||
volumes:
|
||||
- ./flask/app:/var/www/app
|
||||
- ./flask/app/log:/var/log/uwsgi/app
|
31
flask/Dockerfile
Normal file
31
flask/Dockerfile
Normal file
@@ -0,0 +1,31 @@
|
||||
FROM ubuntu:24.04
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y \
|
||||
python3 \
|
||||
python3-pip \
|
||||
python3-venv \
|
||||
uwsgi-plugin-python3 \
|
||||
nginx \
|
||||
supervisor && \
|
||||
echo 'Etc/UTC' >/etc/timezone && \
|
||||
apt-get install -y --reinstall tzdata && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY nginx/flask.conf /etc/nginx/sites-available/
|
||||
COPY supervisor/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||
COPY app/requirements.txt /tmp/requirements.txt
|
||||
|
||||
RUN python3 -m venv venv && chmod +x venv
|
||||
RUN mkdir -p /var/log/nginx/app /var/log/uwsgi/app /var/log/supervisor /var/www/app && \
|
||||
rm /etc/nginx/sites-enabled/default && \
|
||||
ln -s /etc/nginx/sites-available/flask.conf /etc/nginx/sites-enabled/flask.conf && \
|
||||
echo "daemon off;" >> /etc/nginx/nginx.conf && \
|
||||
. venv/bin/activate && \
|
||||
pip3 install -r /tmp/requirements.txt && \
|
||||
chown -R www-data:www-data /var/www/app && \
|
||||
chown -R www-data:www-data /var/log
|
||||
|
||||
CMD ["/usr/bin/supervisord"]
|
174
flask/app/Stats.py
Normal file
174
flask/app/Stats.py
Normal file
@@ -0,0 +1,174 @@
|
||||
import constants as C
|
||||
import dns.resolver
|
||||
import time
|
||||
from collections import Counter
|
||||
from flask import jsonify
|
||||
from itertools import islice
|
||||
from pymongo import MongoClient
|
||||
from functions import asn_name_query
|
||||
|
||||
|
||||
class Stats(object):
|
||||
def __init__(self):
|
||||
self.db = self.db_connect()
|
||||
self.peer_counter = 0
|
||||
self.ipv4_table_size = 0
|
||||
self.ipv6_table_size = 0
|
||||
self.nexthop_ip_counter = 0
|
||||
self.avg_as_path_length = 0
|
||||
self.top_n_peers = []
|
||||
self.cidr_breakdown = []
|
||||
self.communities = []
|
||||
self.peers = []
|
||||
self.customers = []
|
||||
self.customer_count = 0
|
||||
self.customer_ipv4_prefixes = 0
|
||||
self.customer_ipv6_prefixes = 0
|
||||
self.timestamp = self.epoch_to_date(time.time())
|
||||
|
||||
# @property
|
||||
# def peer_counter(self):
|
||||
# return self._peer_counter
|
||||
|
||||
# @peer_counter.setter
|
||||
# def peer_counter(self):
|
||||
# self._peer_counter = len(self.db['bgp'].distinct('nexthop_asn', {'active': True}))
|
||||
|
||||
def db_connect(self):
|
||||
"""Return a connection to the Mongo Database."""
|
||||
client = MongoClient(host='mongodb')
|
||||
return client.bgp
|
||||
|
||||
def take(self, n, iterable):
|
||||
"""Return first n items of the iterable as a list."""
|
||||
return list(islice(iterable, n))
|
||||
|
||||
def peer_count(self):
|
||||
"""Return the number of directly connected ASNs."""
|
||||
return len(self.db['bgp'].distinct('nexthop_asn', {'active': True}))
|
||||
|
||||
def prefix_count(self, version):
|
||||
"""Given the IP version, return the number of prefixes in the database."""
|
||||
return self.db['bgp'].count_documents({'ip_version': version, 'active': True})
|
||||
|
||||
def nexthop_ip_count(self):
|
||||
"""Return the number of unique next hop IPv4 and IPv6 addresses."""
|
||||
return len(self.db['bgp'].distinct('nexthop', {'active': True}))
|
||||
|
||||
def epoch_to_date(self, epoch):
|
||||
"""Given an *epoch* time stamp, return a human readable equivalent."""
|
||||
return time.strftime('%Y-%m-%d %H:%M:%S %Z', time.gmtime(epoch))
|
||||
|
||||
def get_list_of(self, customers=False, peers=False, community=C.CUSTOMER_BGP_COMMUNITY):
|
||||
"""Return a list of prefix dictionaries. Specify which type of prefix to
|
||||
return by setting *customers* or *peers* to True."""
|
||||
if peers:
|
||||
query_results = {prefix['nexthop_asn'] for prefix in self.db['bgp'].find({'active': True})}
|
||||
else: # customers
|
||||
query_results = {prefix['nexthop_asn'] for prefix in
|
||||
self.db['bgp'].find({'communities': community, 'active': True})}
|
||||
|
||||
return [{'asn': asn if asn is not None else C.DEFAULT_ASN, # Set "None" ASNs to default
|
||||
'name': asn_name_query(asn),
|
||||
'ipv4_origin_count': self.db['bgp'].count_documents(
|
||||
{'origin_asn': asn, 'ip_version': 4, 'active': True}),
|
||||
'ipv6_origin_count': self.db['bgp'].count_documents(
|
||||
{'origin_asn': asn, 'ip_version': 6, 'active': True}),
|
||||
'ipv4_nexthop_count': self.db['bgp'].count_documents(
|
||||
{'nexthop_asn': asn, 'ip_version': 4, 'active': True}),
|
||||
'ipv6_nexthop_count': self.db['bgp'].count_documents(
|
||||
{'nexthop_asn': asn, 'ip_version': 6, 'active': True}),
|
||||
'asn_count': len(self.db['bgp'].distinct('as_path.1', {'nexthop_asn': asn, 'active': True}))}
|
||||
for asn in query_results]
|
||||
|
||||
def avg_as_path_len(self, decimal_point_accuracy=2):
|
||||
"""Return the computed average *as_path* length of all prefixes in the
|
||||
database. Using a python *set* to remove any AS prepending."""
|
||||
as_path_counter = 0
|
||||
all_prefixes = list(self.db['bgp'].find({'active': True}))
|
||||
for prefix in all_prefixes:
|
||||
try:
|
||||
as_path_counter += len(set(prefix['as_path'])) # sets remove duplicate ASN prepending
|
||||
except Exception:
|
||||
pass
|
||||
return round(as_path_counter / (len(all_prefixes) * 1.0), decimal_point_accuracy)
|
||||
|
||||
def communities_count(self):
|
||||
"""Return a list of BGP communities and their count"""
|
||||
return [{'community': community,
|
||||
# 'count': self.db['bgp'].count_documents({'communities': {'$regex': str(community)}, 'active': True}),
|
||||
'count': self.db['bgp'].count_documents({'communities': str(community), 'active': True}),
|
||||
'name': None if C.BGP_COMMUNITY_MAP.get(community) is None else C.BGP_COMMUNITY_MAP.get(community)}
|
||||
for community in self.db['bgp'].distinct('communities') if community is not None]
|
||||
|
||||
def cidrs(self):
|
||||
""" Return a list of IPv4 and IPv6 network mask counters."""
|
||||
ipv4_masks = [int(prefix['_id'].split('/', 1)[1])
|
||||
for prefix in self.db['bgp'].find({'ip_version': 4, 'active': True})]
|
||||
ipv6_masks = [int(prefix['_id'].split('/', 1)[1])
|
||||
for prefix in self.db['bgp'].find({'ip_version': 6, 'active': True})]
|
||||
# Use a *Counter* to count masks in the lists, then combine, sort on mask, and return results
|
||||
return sorted(
|
||||
[{'mask': mask,
|
||||
'count': count,
|
||||
'ip_version': 4}
|
||||
for mask, count in list(Counter(ipv4_masks).items())]
|
||||
+
|
||||
[{'mask': mask,
|
||||
'count': count,
|
||||
'ip_version': 6}
|
||||
for mask, count in list(Counter(ipv6_masks).items())], key=lambda x: x['mask'])
|
||||
|
||||
def top_peers(self, count):
|
||||
"""Return a sorted list of top peer dictionaries ordered by prefix count.
|
||||
Limit to *count*."""
|
||||
peers = {peer: self.db['bgp'].count_documents({'nexthop_asn': peer, 'active': True})
|
||||
for peer in self.db['bgp'].distinct('nexthop_asn')}
|
||||
return [{'asn': asn[0],
|
||||
'count': asn[1],
|
||||
'name': asn_name_query(asn[0])}
|
||||
for asn in self.take(count, sorted(peers.items(), key=lambda x: x[1], reverse=True))]
|
||||
|
||||
def get_data(self, json=False):
|
||||
data_dict = {
|
||||
'peer_count': self.peer_counter,
|
||||
'ipv6_table_size': self.ipv6_table_size,
|
||||
'ipv4_table_size': self.ipv4_table_size,
|
||||
'nexthop_ip_count': self.nexthop_ip_counter,
|
||||
'avg_as_path_length': self.avg_as_path_length,
|
||||
'top_n_peers': self.top_n_peers,
|
||||
'cidr_breakdown': self.cidr_breakdown,
|
||||
'communities': self.communities,
|
||||
'peers': self.peers,
|
||||
'customers': self.customers,
|
||||
'customer_count': self.customer_count,
|
||||
'customer_ipv4_prefixes': self.customer_ipv4_prefixes,
|
||||
'customer_ipv6_prefixes': self.customer_ipv6_prefixes,
|
||||
'timestamp': self.timestamp}
|
||||
if json:
|
||||
return jsonify(data_dict)
|
||||
else:
|
||||
return data_dict
|
||||
|
||||
def update_stats(self):
|
||||
self.peer_counter = self.peer_count()
|
||||
self.ipv4_table_size = self.prefix_count(4)
|
||||
self.ipv6_table_size = self.prefix_count(6)
|
||||
self.nexthop_ip_counter = self.nexthop_ip_count()
|
||||
self.timestamp = self.epoch_to_date(time.time())
|
||||
|
||||
def update_advanced_stats(self):
|
||||
self.avg_as_path_length = self.avg_as_path_len()
|
||||
self.top_n_peers = self.top_peers(5)
|
||||
self.cidr_breakdown = self.cidrs()
|
||||
# self.customers = self.get_list_of(customers=True)
|
||||
self.communities = self.communities_count()
|
||||
self.customers = self.get_list_of(customers=True)
|
||||
self.peers = self.get_list_of(peers=True)
|
||||
self.customer_count = len(self.customers)
|
||||
self.customer_ipv4_prefixes = 0
|
||||
self.customer_ipv6_prefixes = 0
|
||||
for customer in self.customers:
|
||||
self.customer_ipv4_prefixes += customer['ipv4_origin_count']
|
||||
self.customer_ipv6_prefixes += customer['ipv6_origin_count']
|
||||
self.timestamp = self.epoch_to_date(time.time())
|
237
flask/app/bgp.py
Normal file
237
flask/app/bgp.py
Normal file
@@ -0,0 +1,237 @@
|
||||
import threading
|
||||
|
||||
from flask import Flask, jsonify, render_template
|
||||
|
||||
import constants as C
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from functions import (asn_name_query, get_ip_json, is_peer, is_transit,
|
||||
reverse_dns_query, dns_query)
|
||||
from Stats import Stats
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config['JSON_SORT_KEYS'] = False
|
||||
app.config['JSONIFY_PRETTYPRINT_REGULAR'] = True
|
||||
|
||||
|
||||
@app.route('/', methods=['GET'])
|
||||
def bgp_index():
|
||||
data = myStats.get_data()
|
||||
top_peers = data['top_n_peers']
|
||||
cidr_breakdown = data['cidr_breakdown']
|
||||
communities = data['communities']
|
||||
peers = data['peers']
|
||||
source_asn = C.DEFAULT_ASN
|
||||
source_asn_name = asn_name_query(C.DEFAULT_ASN)
|
||||
customer_bgp_community = C.CUSTOMER_BGP_COMMUNITY
|
||||
transit_bgp_community = C.TRANSIT_BGP_COMMUNITY
|
||||
peer_bgp_community = C.PEER_BGP_COMMUNITY
|
||||
return render_template('bgp.html', **locals())
|
||||
|
||||
|
||||
@app.route('/bgp/api/v1.0/peers', methods=['GET'])
|
||||
def get_peers():
|
||||
return jsonify(myStats.get_list_of(peers=True))
|
||||
|
||||
|
||||
@app.route('/bgp/api/v1.0/customers', methods=['GET'])
|
||||
def get_customers():
|
||||
return jsonify(myStats.get_list_of(customers=True))
|
||||
|
||||
|
||||
@app.route('/bgp/api/v1.0/ip/<ip>', methods=['GET'])
|
||||
def get_ip(ip):
|
||||
return jsonify(get_ip_json(ip, include_history=False))
|
||||
|
||||
|
||||
@app.route('/bgp/api/v1.0/communities', methods=['GET'])
|
||||
def get_communities():
|
||||
return jsonify(myStats.communities_count())
|
||||
|
||||
|
||||
@app.route('/bgp/api/v1.0/ip/<ip>/history', methods=['GET'])
|
||||
def get_history(ip):
|
||||
return jsonify(get_ip_json(ip, include_history=True))
|
||||
|
||||
|
||||
@app.route('/bgp/api/v1.0/asn/<int:asn>', methods=['GET'])
|
||||
def get_asn_prefixes(asn):
|
||||
db = myStats.db
|
||||
prefixes = []
|
||||
|
||||
if asn == C.DEFAULT_ASN:
|
||||
routes = list(db['bgp'].find({'origin_asn': None, 'active': True}))
|
||||
else:
|
||||
routes = list(db['bgp'].find({'origin_asn': asn, 'active': True}))
|
||||
|
||||
for prefix in routes:
|
||||
prefixes.append({'prefix': prefix['_id'],
|
||||
'is_transit': is_transit(prefix),
|
||||
'origin_asn': prefix['origin_asn'],
|
||||
'name': asn_name_query(asn),
|
||||
'nexthop_ip': prefix['nexthop'],
|
||||
'nexthop_ip_dns': reverse_dns_query(prefix['nexthop']),
|
||||
'nexthop_asn': prefix['nexthop_asn'],
|
||||
'as_path': prefix['as_path'],
|
||||
'updated': prefix['age']
|
||||
})
|
||||
|
||||
return jsonify({'asn': asn,
|
||||
'name': asn_name_query(asn),
|
||||
'origin_prefix_count': len(routes),
|
||||
'is_peer': is_peer(asn),
|
||||
'origin_prefix_list': prefixes})
|
||||
|
||||
|
||||
@app.route('/bgp/api/v1.0/stats', methods=['GET'])
|
||||
def get_stats():
|
||||
return myStats.get_data(json=True)
|
||||
|
||||
|
||||
@app.route('/bgp/api/v1.0/asn/<int:asn>/downstream', methods=['GET'])
|
||||
def get_downstream_asns(asn):
|
||||
db = myStats.db
|
||||
asn_list = []
|
||||
large_query = 200
|
||||
downstream_asns = db['bgp'].distinct('as_path.1', {'nexthop_asn': asn, 'active': True})
|
||||
for downstream in downstream_asns:
|
||||
if len(downstream_asns) > large_query:
|
||||
dns_name = "(LARGE QUERY - DNS LOOKUP DISABLED)"
|
||||
else:
|
||||
dns_name = asn_name_query(downstream)
|
||||
asn_list.append({'asn': downstream, 'name': dns_name})
|
||||
|
||||
sorted_asn_list = sorted(asn_list, key=lambda k: k['asn'])
|
||||
|
||||
return jsonify({'asn': asn,
|
||||
'name': asn_name_query(asn),
|
||||
'downstream_asns_count': len(asn_list),
|
||||
'downstream_asns': sorted_asn_list})
|
||||
|
||||
|
||||
@app.route('/bgp/api/v1.0/asn/<int:asn>/originated', methods=['GET'])
|
||||
def get_originated_prefixes(asn):
|
||||
db = myStats.db
|
||||
originated = []
|
||||
prefixes = db['bgp'].find({'origin_asn': asn, 'active': True})
|
||||
for prefix in prefixes:
|
||||
originated.append(prefix['_id'])
|
||||
|
||||
return jsonify({'asn': asn,
|
||||
'name': asn_name_query(asn),
|
||||
'originated_prefix_count': len(originated),
|
||||
'originated_prefix_list': originated})
|
||||
|
||||
|
||||
@app.route('/bgp/api/v1.0/asn/<int:asn>/originated/<version>', methods=['GET'])
|
||||
def get_originated_prefixes_version(asn, version):
|
||||
db = myStats.db
|
||||
originated = []
|
||||
v = 4
|
||||
if version.lower() == 'ipv6':
|
||||
v = 6
|
||||
prefixes = db['bgp'].find({'origin_asn': asn, 'ip_version': v, 'active': True})
|
||||
for prefix in prefixes:
|
||||
originated.append(prefix['_id'])
|
||||
|
||||
return jsonify({'asn': asn,
|
||||
'name': asn_name_query(asn),
|
||||
'originated_prefix_count': len(originated),
|
||||
'originated_prefix_list': originated})
|
||||
|
||||
|
||||
@app.route('/bgp/api/v1.0/asn/<int:asn>/nexthop', methods=['GET'])
|
||||
def get_nexthop_prefixes(asn):
|
||||
db = myStats.db
|
||||
nexthop = []
|
||||
prefixes = db['bgp'].find({'nexthop_asn': asn, 'active': True})
|
||||
for prefix in prefixes:
|
||||
nexthop.append(prefix['_id'])
|
||||
|
||||
return jsonify({'asn': asn,
|
||||
'name': asn_name_query(asn),
|
||||
'nexthop_prefix_count': len(nexthop),
|
||||
'nexthop_prefix_list': nexthop})
|
||||
|
||||
|
||||
@app.route('/bgp/api/v1.0/asn/<int:asn>/nexthop/<version>', methods=['GET'])
|
||||
def get_nexthop_prefixes_version(asn, version):
|
||||
db = myStats.db
|
||||
nexthop = []
|
||||
v = 4
|
||||
if version.lower() == 'ipv6':
|
||||
v = 6
|
||||
prefixes = db['bgp'].find({'nexthop_asn': asn, 'ip_version': v, 'active': True})
|
||||
for prefix in prefixes:
|
||||
nexthop.append(prefix['_id'])
|
||||
|
||||
return jsonify({'asn': asn,
|
||||
'name': asn_name_query(asn),
|
||||
'nexthop_prefix_count': len(nexthop),
|
||||
'nexthop_prefix_list': nexthop})
|
||||
|
||||
|
||||
@app.route('/bgp/api/v1.0/asn/<int:asn>/transit', methods=['GET'])
|
||||
def get_transit_prefixes(asn):
|
||||
db = myStats.db
|
||||
all_asns = db['bgp'].find({'active': True})
|
||||
prefixes = []
|
||||
|
||||
for prefix in all_asns:
|
||||
if prefix['as_path']:
|
||||
if asn in prefix['as_path']:
|
||||
prefixes.append(prefix['_id'])
|
||||
else:
|
||||
pass
|
||||
else:
|
||||
pass
|
||||
|
||||
return jsonify({'asn': asn,
|
||||
'name': asn_name_query(asn),
|
||||
'transit_prefix_count': len(prefixes),
|
||||
'transit_prefix_list': prefixes})
|
||||
|
||||
|
||||
@app.route('/bgp/api/v1.0/domain/<domain>', methods=['GET'])
|
||||
def get_domain(domain):
|
||||
domain = domain.lower()
|
||||
org = domain.split('.')[-2]
|
||||
name_servers = dns_query(domain, 'NS')
|
||||
soa = dns_query(domain, 'SOA')
|
||||
local_ns = ''
|
||||
if org in soa.lower():
|
||||
local_ns = soa.lower()
|
||||
for ns in name_servers:
|
||||
if org in ns.lower():
|
||||
local_ns = ns.lower()
|
||||
if local_ns == '':
|
||||
return jsonify({})
|
||||
else:
|
||||
domain_ip = str(dns_query(local_ns))
|
||||
ip_data = get_ip_json(domain_ip)
|
||||
asn = ip_data.get('origin_asn')
|
||||
db = myStats.db
|
||||
originated = []
|
||||
prefixes = db['bgp'].find({'origin_asn': asn, 'active': True})
|
||||
for prefix in prefixes:
|
||||
originated.append(prefix['_id'])
|
||||
|
||||
return jsonify({'domain': domain,
|
||||
'A Record': dns_query(domain),
|
||||
'SOA/NS Record': local_ns,
|
||||
'SOA/NS IP': domain_ip,
|
||||
'asn': asn,
|
||||
'name': asn_name_query(asn),
|
||||
'originated_prefix_count': len(originated),
|
||||
'originated_prefix_list': originated})
|
||||
|
||||
|
||||
sched = BackgroundScheduler()
|
||||
myStats = Stats()
|
||||
threading.Thread(target=myStats.update_stats).start()
|
||||
threading.Thread(target=myStats.update_advanced_stats).start()
|
||||
sched.add_job(myStats.update_stats, 'interval', seconds=60)
|
||||
sched.add_job(myStats.update_advanced_stats, 'interval', seconds=300)
|
||||
sched.start()
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True)
|
31
flask/app/constants.py
Normal file
31
flask/app/constants.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# DEFAULTS - UPDATE ACCORDINGLY
|
||||
DEFAULT_ASN = 400848
|
||||
CUSTOMER_BGP_COMMUNITY = '3701:370' # Prefixes learned from directly connected customers
|
||||
TRANSIT_BGP_COMMUNITY = '3701:380' # Prefixes learned from *paid* transit providers
|
||||
PEER_BGP_COMMUNITY = '3701:39' # Community Prefix (starts with) for Prefixes learned from bilateral peers and exchanges
|
||||
BGP_COMMUNITY_MAP = {
|
||||
'3701:111': 'Level3-Prepend-1',
|
||||
'3701:112': 'Level3-Prepend-2',
|
||||
'3701:113': 'Level3-SEAT-Depref',
|
||||
'3701:114': 'Level3-WSAC-Depref',
|
||||
'3701:121': 'Level3-WSAC-Prepend-1',
|
||||
'3701:122': 'Level3-WSAC-Prepend-2',
|
||||
'3701:370': 'Customers',
|
||||
'3701:371': 'Customers-NO-I2-RE',
|
||||
'3701:372': 'Customers-NO-I2-CP',
|
||||
'3701:380': 'Transit',
|
||||
'3701:381': 'Level3-SEAT',
|
||||
'3701:382': 'Level3-WSAC',
|
||||
'3701:390': 'OIX',
|
||||
'3701:391': 'I2-RE',
|
||||
'3701:392': 'NWAX',
|
||||
'3701:393': 'PNWGP',
|
||||
'3701:394': 'I2-CPS',
|
||||
'3701:395': 'SeattleIX',
|
||||
'3701:500': 'PT-ODE-USERS',
|
||||
'3701:501': 'PT-ODE-PROVIDERS',
|
||||
'3701:666': 'BH-LOCAL',
|
||||
'64496:0': 'Cymru-UTRS',
|
||||
'65333:888': 'Cymru-BOGONs',
|
||||
'65535:65281': 'No-Export',
|
||||
}
|
145
flask/app/functions.py
Normal file
145
flask/app/functions.py
Normal file
@@ -0,0 +1,145 @@
|
||||
import ipaddress
|
||||
from functools import cache
|
||||
|
||||
import dns.resolver
|
||||
from pymongo.database import Database
|
||||
|
||||
import constants as C
|
||||
from flask import jsonify, request
|
||||
from pymongo import MongoClient
|
||||
|
||||
|
||||
@cache
|
||||
def db_connect() -> Database:
|
||||
"""Return a connection to the Mongo Database."""
|
||||
client = MongoClient(host='mongodb')
|
||||
return client["bgp"]
|
||||
|
||||
|
||||
def find_network(ip, netmask):
|
||||
"""Given an IPv4 or IPv6 address, recursively search for and return the most
|
||||
specific prefix in the MongoDB collection that is active.
|
||||
"""
|
||||
try:
|
||||
db = db_connect()
|
||||
network = str(ipaddress.ip_network(ipaddress.ip_address(ip)).supernet(new_prefix=netmask))
|
||||
result = db['bgp'].find_one({'_id': network, 'active': True})
|
||||
if result is not None:
|
||||
return result
|
||||
elif netmask == 0:
|
||||
return None
|
||||
else:
|
||||
return find_network(ip, netmask - 1)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def is_peer(asn):
|
||||
"""Is *asn* in the list of directy connected ASNs."""
|
||||
db = db_connect()
|
||||
if asn in db['bgp'].distinct('nexthop_asn'):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def is_transit(prefix, transit_bgp_community=C.TRANSIT_BGP_COMMUNITY):
|
||||
"""Is the *prefix* counted as transit?"""
|
||||
if C.TRANSIT_BGP_COMMUNITY in prefix['communities']:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def reverse_dns_query(ip):
|
||||
"""Given an *ip*, return the reverse dns."""
|
||||
try:
|
||||
addr = dns.reversename.from_address(str(ip))
|
||||
resolver = dns.resolver.Resolver()
|
||||
return str(resolver.resolve(addr, 'PTR')[0])[:-1]
|
||||
except Exception:
|
||||
return '(DNS Error)'
|
||||
|
||||
|
||||
def dns_query(name, type='A'):
|
||||
"""Given a *name*, return the ip dns."""
|
||||
try:
|
||||
# addr = dns.reversename.from_address(str(ip))
|
||||
resolver = dns.resolver.Resolver()
|
||||
answers = resolver.resolve(str(name), type)
|
||||
if type == 'A':
|
||||
return str(answers[0])
|
||||
elif type == 'NS':
|
||||
domains = []
|
||||
for record in answers:
|
||||
domains.append(str(record.target))
|
||||
return domains
|
||||
elif type == 'SOA':
|
||||
return str(answers[0]).split()[0]
|
||||
except Exception:
|
||||
return '(DNS Error)'
|
||||
|
||||
|
||||
def asn_name_query(asn):
|
||||
"""Given an *asn*, return the name."""
|
||||
if asn is None:
|
||||
asn = C.DEFAULT_ASN
|
||||
if 64496 <= asn <= 64511:
|
||||
return ('RFC5398 - Private Use ASN')
|
||||
if 64512 <= asn <= 65535 or 4200000000 <= asn <= 4294967295:
|
||||
return ('RFC6996 - Private Use ASN')
|
||||
try:
|
||||
query = 'as{number}.asn.cymru.com'.format(number=str(asn))
|
||||
resolver = dns.resolver.Resolver()
|
||||
answers = resolver.resolve(query, 'TXT')
|
||||
for rdata in answers:
|
||||
return (str(rdata).split('|')[-1].split(',', 2)[0].strip())
|
||||
except Exception:
|
||||
return '(DNS Error)'
|
||||
|
||||
|
||||
def get_ip_json(ip, include_history=True):
|
||||
if '/' in ip:
|
||||
ip = ip.lstrip().rstrip().split('/')[0]
|
||||
try:
|
||||
if ipaddress.ip_address(ip).version == 4:
|
||||
network = find_network(ip, netmask=32)
|
||||
elif ipaddress.ip_address(ip).version == 6:
|
||||
network = find_network(ip, netmask=128)
|
||||
except Exception:
|
||||
try:
|
||||
ipadr = dns_query(ip).strip()
|
||||
if ipaddress.ip_address(ipadr).version == 4:
|
||||
network = find_network(ipadr, netmask=32)
|
||||
elif ipaddress.ip_address(ipadr).version == 6:
|
||||
network = find_network(ipadr, netmask=128)
|
||||
except Exception as e:
|
||||
return jsonify(str(e))
|
||||
if network:
|
||||
if include_history:
|
||||
history = network['history']
|
||||
else:
|
||||
history = request.base_url + '/history'
|
||||
return {'prefix': network['_id'],
|
||||
'ip_version': network['ip_version'],
|
||||
'is_transit': is_transit(network),
|
||||
'origin_asn': network['origin_asn'],
|
||||
'name': asn_name_query(network['origin_asn']),
|
||||
'nexthop': network['nexthop'],
|
||||
'nexthop_ip_dns': reverse_dns_query(network['nexthop']),
|
||||
'nexthop_asn': network['nexthop_asn'],
|
||||
'as_path': network['as_path'],
|
||||
'med': network['med'],
|
||||
'local_pref': network['local_pref'],
|
||||
'communities': network['communities'],
|
||||
'route_origin': network['route_origin'],
|
||||
'atomic_aggregate': network['atomic_aggregate'],
|
||||
'aggregator_as': network['aggregator_as'],
|
||||
'aggregator_address': network['aggregator_address'],
|
||||
'originator_id': network['originator_id'],
|
||||
'originator_id_dns': reverse_dns_query(network['originator_id']),
|
||||
'cluster_list': network['cluster_list'],
|
||||
'age': network['age'],
|
||||
'history': history}
|
||||
else:
|
||||
return {}
|
9
flask/app/requirements.txt
Normal file
9
flask/app/requirements.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
uwsgi >= 2.0.26
|
||||
flask >= 3.0.3
|
||||
requests >= 2.32.3
|
||||
apscheduler >= 3.10.4
|
||||
pytz >= 2024.1
|
||||
|
||||
pymongo >= 4.8.0
|
||||
dnspython >= 2.6.1
|
||||
ipaddress >= 1.0.23
|
3989
flask/app/static/css/custom.css
Normal file
3989
flask/app/static/css/custom.css
Normal file
File diff suppressed because it is too large
Load Diff
6
flask/app/static/js/jquery.sparkline.min.js
vendored
Normal file
6
flask/app/static/js/jquery.sparkline.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
569
flask/app/templates/bgp.html
Normal file
569
flask/app/templates/bgp.html
Normal file
@@ -0,0 +1,569 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<!-- Meta, title, CSS, favicons, etc. -->
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<title>BGP Dashboard</title>
|
||||
|
||||
<!-- Bootstrap -->
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
|
||||
<!-- Font Awesome -->
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" integrity="sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN" crossorigin="anonymous">
|
||||
<!-- Custom Theme Style -->
|
||||
<link href="../static/css/custom.css" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body class="nav-md">
|
||||
<div class="container body">
|
||||
<div class="main_container">
|
||||
|
||||
|
||||
<!-- top navigation -->
|
||||
<div class="top_nav">
|
||||
<div class="nav_menu">
|
||||
<nav>
|
||||
<div class="navbar-left" style="padding-left: 30px;">
|
||||
<h1 style="color: #34495e;">ASN{{source_asn}}</h1>
|
||||
<h2><i>{{source_asn_name}}</i></h2>
|
||||
</div>
|
||||
<div class="title_right">
|
||||
<div class="col-md-2 col-sm-2 col-xs-12 form-group pull-right top_search">
|
||||
<form onsubmit="location.href='/bgp/api/v1.0/asn/' + document.getElementById('asnInput').value; return false;" class="input-group">
|
||||
<input type="text" id="asnInput" class="form-control" placeholder="ASN Search...">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" type="submit">Go!</button>
|
||||
</span>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="title_right">
|
||||
<div class="col-md-2 col-sm-2 col-xs-12 form-group pull-right top_search">
|
||||
<form onsubmit="location.href='/bgp/api/v1.0/ip/' + document.getElementById('ipInput').value.split('/')[0]; return false;" class="input-group">
|
||||
<input type="text" id="ipInput" class="form-control" placeholder="IP/DNS Search...">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" type="submit">Go!</button>
|
||||
</span>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /top navigation -->
|
||||
|
||||
<!-- page content -->
|
||||
<div class="right_col" role="main">
|
||||
<div class="">
|
||||
<div class="row top_tiles">
|
||||
<div class="animated flipInY col-lg-3 col-md-3 col-sm-6 col-xs-12">
|
||||
<div class="tile-stats" id="peers_box">
|
||||
<div class="icon"><i id="peersIDIcon"></i></div>
|
||||
<div class="count" id="peersID">0</div>
|
||||
<h3>Peer Count</h3>
|
||||
<div>
|
||||
<p class="count_bottom pull-right" id="peersIDChangeTime"></p>
|
||||
<p>Active egress BGP peers</p>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
<p class="peers_count_sparkline pull-right" style="margin-right: 5px; margin-left: 5px;">Loading..</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="animated flipInY col-lg-3 col-md-3 col-sm-6 col-xs-12">
|
||||
<div class="tile-stats">
|
||||
<div class="icon"><i id="ipv4TableSizeIcon"></i></div>
|
||||
<div class="count" id="ipv4TableSize">0</div>
|
||||
<h3>IPv4 Prefixes</h3>
|
||||
<div>
|
||||
<p class="count_bottom pull-right" id="ipv4TableSizeChangeTime"></p>
|
||||
<p>IPv4 BGP Table Size</p>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
<p class="ipv4_table_sparkline pull-right" style="margin-right: 5px; margin-left: 5px;">Loading..</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="animated flipInY col-lg-3 col-md-3 col-sm-6 col-xs-12">
|
||||
<div class="tile-stats">
|
||||
<div class="icon"><i id="ipv6TableSizeIcon"></i></div>
|
||||
<div class="count" id="ipv6TableSize">0</div>
|
||||
<h3>IPv6 Prefixes</h3>
|
||||
<div>
|
||||
<p class="count_bottom pull-right" id="ipv6TableSizeChangeTime"></p>
|
||||
<p>IPv6 BGP Table Size</p>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
<p class="ipv6_table_sparkline pull-right" style="margin-right: 5px; margin-left: 5px;">Loading..</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="animated flipInY col-lg-3 col-md-3 col-sm-6 col-xs-12">
|
||||
<div class="tile-stats">
|
||||
<div class="icon"><i id="nexthopIPCountIcon"></i></div>
|
||||
<div class="count" id="nexthopIPCount">0</div>
|
||||
<h3>Next Hop Addresses</h3>
|
||||
<div>
|
||||
<p class="count_bottom pull-right" id="nexthopIPCountChangeTime"></p>
|
||||
<p>Unique Egress IP Addresses</p>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
<p class="nexthop_count_sparkline pull-right" style="margin-right: 5px; margin-left: 5px;">Loading..</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Second Row -->
|
||||
<div class="row top_tiles">
|
||||
<div class="animated flipInY col-lg-3 col-md-3 col-sm-6 col-xs-12">
|
||||
<div class="tile-stats">
|
||||
<div class="icon"><i id="avgAsPathLengthIcon"></i></div>
|
||||
<div class="count" id="avgAsPathLength">0</div>
|
||||
<h3>Average AS Path Length</h3>
|
||||
<div>
|
||||
<p class="count_bottom pull-right" id="avgAsPathLengthChangeTime"></p>
|
||||
<p>ASN hops to destination (lower is better)</p>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
<p class="avg_as_path_length_sparkline pull-right" style="margin-right: 5px; margin-left: 5px;">Loading..</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="animated flipInY col-lg-3 col-md-3 col-sm-6 col-xs-12">
|
||||
<div class="tile-stats">
|
||||
<div class="icon"><i id="customerIPv4PrefixesIcon"></i></div>
|
||||
<div class="count" id="customerIPv4Prefixes">0</div>
|
||||
<h3>Customer IPv4 Prefixes</h3>
|
||||
<div>
|
||||
<p class="count_bottom pull-right" id="customerIPv4PrefixesChangeTime"></p>
|
||||
<p>Customer & Orginated IPv4 Prefix Count</p>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
<p class="customer_ipv4_prefixes_sparkline pull-right" style="margin-right: 5px; margin-left: 5px;">Loading..</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="animated flipInY col-lg-3 col-md-3 col-sm-6 col-xs-12">
|
||||
<div class="tile-stats">
|
||||
<div class="icon"><i id="customerIPv6PrefixesIcon"></i></div>
|
||||
<div class="count" id="customerIPv6Prefixes">0</div>
|
||||
<h3>Customer IPv6 Prefixes</h3>
|
||||
<div>
|
||||
<p class="count_bottom pull-right" id="customerIPv6PrefixesChangeTime"></p>
|
||||
<p>Customer & Orginated IPv6 Prefix Count</p>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
<p class="customer_ipv6_prefixes_sparkline pull-right" style="margin-right: 5px; margin-left: 5px;">Loading..</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="animated flipInY col-lg-3 col-md-3 col-sm-6 col-xs-12">
|
||||
<div class="tile-stats">
|
||||
<div class="icon"><i id="customerCountIcon"></i></div>
|
||||
<div class="count" id="customerCount">0</div>
|
||||
<h3>BGP Customers</h3>
|
||||
<div>
|
||||
<p class="count_bottom pull-right" id="customerCountChangeTime"></p>
|
||||
<p>Downstream BGP connected ASNs</p>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
<p class="customer_count_sparkline pull-right" style="margin-right: 5px; margin-left: 5px;">Loading..</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="x_panel">
|
||||
<div class="x_title">
|
||||
<h2>Peer Data <small>Updated every 300s</small></h2>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="x_content">
|
||||
<div class="col-md-9 col-sm-12 col-xs-12">
|
||||
<div class="demo-container">
|
||||
<table id="datatable" class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ASN</th>
|
||||
<th>Name</th>
|
||||
<th>IPv4 Prefixes<br>Originated</th>
|
||||
<th>IPv4 Prefixes<br>NextHop</th>
|
||||
<th>IPv6 Prefixes<br>Originated</th>
|
||||
<th>IPv6 Prefixes<br>NextHop</th>
|
||||
<th>Downstream<br>ASN Count</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 col-sm-12 col-xs-12">
|
||||
<div>
|
||||
<div class="x_title">
|
||||
<h2>Top Peers (Prefixes Received)</h2>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<ul class="list-unstyled top_profiles scroll-view">
|
||||
{% for peer in top_peers %}
|
||||
<li class="media event">
|
||||
<a class="pull-left border-blue profile_thumb">
|
||||
<i class="fa fa-arrows-alt blue"></i>
|
||||
</a>
|
||||
<div class="media-body">
|
||||
<a class="title" href="#">ASN {{ peer.asn }}</a>
|
||||
<p><strong>{{ '{0:,}'.format(peer.count) }}</strong> Prefixes </p>
|
||||
<p> <small>{{ peer.name }}</small>
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="x_panel">
|
||||
<div class="x_title">
|
||||
<h2>BGP Prefix Distributions <small>Prefix Count</small></h2>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="x_content">
|
||||
<div class="row" style="border-bottom: 0px solid #E0E0E0; padding-bottom: 5px; margin-bottom: 5px;">
|
||||
<div class="col-md-12">
|
||||
<div class="row" style="text-align: center;">
|
||||
<div id="graph_donut1" class="col-md-4" style="width:50%; height:180px;">
|
||||
<h4 style="margin:0">Peering Prefix Count</h4>
|
||||
</div>
|
||||
<div id="graph_donut2" class="col-md-4" style="width:50%; height:180px;">
|
||||
<h4 style="margin:0">Transit vs. Peering</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<!-- bar chart -->
|
||||
<div class="col-md-6 col-sm-6 col-xs-12">
|
||||
<div class="x_panel">
|
||||
<div class="x_title">
|
||||
<h2>IPv4 Network Mask <small>Prefix Count</small></h2>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="x_content">
|
||||
<div id="graph_bar" style="width:100%; height:280px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /bar charts -->
|
||||
|
||||
<!-- bar charts group -->
|
||||
<div class="col-md-6 col-sm-6 col-xs-12">
|
||||
<div class="x_panel">
|
||||
<div class="x_title">
|
||||
<h2>IPv6 Network Mask <small>Prefix Count</small></h2>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="x_content1">
|
||||
<div id="graph_bar_group" style="width:100%; height:280px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
<!-- /bar charts group -->
|
||||
<div class="clearfix"></div>
|
||||
<!-- /bar charts group -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /page content -->
|
||||
|
||||
<!-- footer content -->
|
||||
<footer>
|
||||
<div class="pull-right">
|
||||
<a href="https://github.com/rhicks/bgp-dashboard">BGP Dashboard</a> at Github
|
||||
<div class="clearfix"></div>
|
||||
<a href="https://github.com/puikinsh/gentelella">Gentelella</a> - Bootstrap Admin Template by <a href="https://colorlib.com">Colorlib</a>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</footer>
|
||||
<!-- /footer content -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- jQuery -->
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
|
||||
<!-- Bootstrap -->
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
|
||||
<!-- jQuery Sparklines -->
|
||||
<script src="../static/js/jquery.sparkline.min.js"></script>
|
||||
<!-- bootstrap-daterangepicker -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.17.1/moment.min.js" integrity="sha256-Gn7MUQono8LUxTfRA0WZzJgTua52Udm1Ifrk5421zkA=" crossorigin="anonymous"></script>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/raphael/2.1.0/raphael-min.js"></script>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/datatables/1.10.13/js/jquery.dataTables.min.js" integrity="sha256-yWA356lDhruy1J8jGncaMWKAPYDbK47OKb0uT/aELLc=" crossorigin="anonymous"></script>
|
||||
|
||||
|
||||
<!-- Custom Theme Scripts -->
|
||||
<!-- <script src="../static/js/custom.min.js"></script> -->
|
||||
|
||||
<script>
|
||||
function up_or_down_checker(css_tag, data, json_tag)
|
||||
{
|
||||
var myDateFormat = moment().format('MMM D YYYY, H:mm:ss');
|
||||
var css_tag_id = '#' + css_tag;
|
||||
var css_tag_icon = css_tag_id + 'Icon';
|
||||
var css_tag_change_time = css_tag_id + 'ChangeTime';
|
||||
|
||||
if (jQuery(css_tag_id).text().replace(/\,/g,'') > data[json_tag])
|
||||
{
|
||||
jQuery(css_tag_icon).removeClass();
|
||||
jQuery(css_tag_icon).addClass("red fa fa-arrow-circle-down");
|
||||
diff = data[json_tag] - jQuery(css_tag_id).text().replace(/\,/g,'');
|
||||
diff = diff.toLocaleString('en-Us', {minimumFractiondigits: 0});
|
||||
jQuery(css_tag_change_time).html('<i class="red">' + diff + '</i> at ' + myDateFormat + ' ');
|
||||
}
|
||||
else if (jQuery(css_tag_id).text().replace(/\,/g,'') < data[json_tag])
|
||||
{
|
||||
jQuery(css_tag_icon).removeClass();
|
||||
jQuery(css_tag_icon).addClass("green fa fa-arrow-circle-up");
|
||||
diff = data[json_tag] - jQuery(css_tag_id).text().replace(/\,/g,'');
|
||||
diff = diff.toLocaleString('en-Us', {minimumFractiondigits: 0});
|
||||
jQuery(css_tag_change_time).html('<i class="green"> +' + diff + '</i> at ' + myDateFormat + ' ');
|
||||
}
|
||||
}
|
||||
|
||||
function update_sparklines(data, how_many_times)
|
||||
{
|
||||
while (how_many_times > 0)
|
||||
{
|
||||
peer_count_sparkline_data.push(data['peer_count']);
|
||||
ipv4_table_sparkline_data.push(data['ipv4_table_size']);
|
||||
ipv6_table_sparkline_data.push(data['ipv6_table_size']);
|
||||
nexthop_count_sparkline_data.push(data['nexthop_ip_count']);
|
||||
avg_as_path_length_data.push(data['avg_as_path_length']);
|
||||
customer_count_data.push(data['customer_count']);
|
||||
customer_ipv4_prefix_data.push(data['customer_ipv4_prefixes']);
|
||||
customer_ipv6_prefix_data.push(data['customer_ipv6_prefixes']);
|
||||
how_many_times--;
|
||||
}
|
||||
}
|
||||
|
||||
var stats = jQuery.getJSON("/bgp/api/v1.0/stats")
|
||||
var peer_count_sparkline_data = [];
|
||||
var ipv4_table_sparkline_data = [];
|
||||
var ipv6_table_sparkline_data = [];
|
||||
var nexthop_count_sparkline_data = [];
|
||||
var avg_as_path_length_data = [];
|
||||
var customer_count_data = [];
|
||||
var customer_ipv4_prefix_data = [];
|
||||
var customer_ipv6_prefix_data = [];
|
||||
|
||||
var peers_table = jQuery('#datatable').DataTable({
|
||||
ajax: {
|
||||
url: "/bgp/api/v1.0/stats",
|
||||
dataSrc: "peers"
|
||||
},
|
||||
columns: [
|
||||
{ data: "asn", render: function (asn) {return '<a href=/bgp/api/v1.0/asn/'+asn+'>'+asn+'</a>';} },
|
||||
{ data: "name" },
|
||||
{ data: "ipv4_origin_count", render: function (data, type, full, meta) {return '<a href=/bgp/api/v1.0/asn/'+full.asn+'/originated/ipv4>'+full.ipv4_origin_count+'</a>';} },
|
||||
{ data: "ipv4_nexthop_count", render: function (data, type, full, meta) {return '<a href=/bgp/api/v1.0/asn/'+full.asn+'/nexthop/ipv4>'+full.ipv4_nexthop_count+'</a>';} },
|
||||
{ data: "ipv6_origin_count", render: function (data, type, full, meta) {return '<a href=/bgp/api/v1.0/asn/'+full.asn+'/originated/ipv6>'+full.ipv6_origin_count+'</a>';} },
|
||||
{ data: "ipv6_nexthop_count", render: function (data, type, full, meta) {return '<a href=/bgp/api/v1.0/asn/'+full.asn+'/nexthop/ipv6>'+full.ipv6_nexthop_count+'</a>';} },
|
||||
{ data: "asn_count", render: function (data, type, full, meta) {return '<a href=/bgp/api/v1.0/asn/'+full.asn+'/downstream>'+full.asn_count+'</a>';} }
|
||||
],
|
||||
"oLanguage":
|
||||
{
|
||||
"sSearch": "Filter Table: "
|
||||
},
|
||||
paging: false,
|
||||
scrollY: 400
|
||||
});
|
||||
|
||||
|
||||
function update_counters()
|
||||
{
|
||||
update_counters.count = update_counters.count || 0;
|
||||
jQuery.getJSON("/bgp/api/v1.0/stats", function(data)
|
||||
{
|
||||
update_sparklines(data, 1)
|
||||
if (update_counters.count > 0) //only update times after the first run
|
||||
{
|
||||
up_or_down_checker('peersID', data, 'peer_count');
|
||||
up_or_down_checker('ipv4TableSize', data, 'ipv4_table_size');
|
||||
up_or_down_checker('ipv6TableSize', data, 'ipv6_table_size');
|
||||
up_or_down_checker('nexthopIPCount', data, 'nexthop_ip_count');
|
||||
up_or_down_checker('avgAsPathLength', data, 'avg_as_path_length');
|
||||
up_or_down_checker('customerCount', data, 'customer_count');
|
||||
up_or_down_checker('customerIPv4Prefixes', data, 'customer_ipv4_prefixes');
|
||||
up_or_down_checker('customerIPv6Prefixes', data, 'customer_ipv6_prefixes');
|
||||
}
|
||||
|
||||
update_counters.count = 1;
|
||||
sparkline_max_width = $("div#peers_box.tile-stats").width() - 10
|
||||
sparkline_data_width = peer_count_sparkline_data.length
|
||||
if (sparkline_data_width < sparkline_max_width)
|
||||
{
|
||||
sparkline_width = sparkline_data_width
|
||||
}
|
||||
else
|
||||
{
|
||||
sparkline_width = sparkline_max_width
|
||||
}
|
||||
jQuery('#peersID').text(data['peer_count'].toLocaleString('en-US', {minimumFractionDigits: 0}));
|
||||
jQuery('#ipv4TableSize').text(data['ipv4_table_size'].toLocaleString('en-US', {minimumFractionDigits: 0}));
|
||||
jQuery('#ipv6TableSize').text(data['ipv6_table_size'].toLocaleString('en-US', {minimumFractionDigits: 0}));
|
||||
jQuery('#nexthopIPCount').text(data['nexthop_ip_count'].toLocaleString('en-US', {minimumFractionDigits: 0}));
|
||||
jQuery('#avgAsPathLength').text(data['avg_as_path_length']);
|
||||
jQuery('#customerCount').text(data['customer_count']);
|
||||
jQuery('#customerIPv4Prefixes').text(data['customer_ipv4_prefixes']);
|
||||
jQuery('#customerIPv6Prefixes').text(data['customer_ipv6_prefixes']);
|
||||
jQuery('.peers_count_sparkline').sparkline(peer_count_sparkline_data, {width: sparkline_width});
|
||||
jQuery('.ipv4_table_sparkline').sparkline(ipv4_table_sparkline_data, {width: sparkline_width});
|
||||
jQuery('.ipv6_table_sparkline').sparkline(ipv6_table_sparkline_data, {width: sparkline_width});
|
||||
jQuery('.nexthop_count_sparkline').sparkline(nexthop_count_sparkline_data, {width: sparkline_width});
|
||||
jQuery('.avg_as_path_length_sparkline').sparkline(avg_as_path_length_data, {width: sparkline_width});
|
||||
jQuery('.customer_count_sparkline').sparkline(customer_count_data, {width: sparkline_width});
|
||||
jQuery('.customer_ipv4_prefixes_sparkline').sparkline(customer_ipv4_prefix_data, {width: sparkline_width});
|
||||
jQuery('.customer_ipv6_prefixes_sparkline').sparkline(customer_ipv6_prefix_data, {width: sparkline_width});
|
||||
|
||||
|
||||
//$('.dynamicbar').sparkline(myvalues, {type: 'bar', barColor: 'green'} );
|
||||
console.log("sparkline_data_width: " + sparkline_data_width)
|
||||
console.log("sparkline_width: " + sparkline_width)
|
||||
console.log("sparkline_max_width: " + sparkline_max_width)
|
||||
if (sparkline_data_width > 1)
|
||||
{
|
||||
//peer_count_sparkline_data.shift();
|
||||
peer_count_sparkline_data.splice(0, (sparkline_data_width - sparkline_max_width))
|
||||
//ipv4_table_sparkline_data.shift();
|
||||
ipv4_table_sparkline_data.splice(0, (sparkline_data_width - sparkline_max_width))
|
||||
// ipv6_table_sparkline_data.shift();
|
||||
ipv6_table_sparkline_data.splice(0, (sparkline_data_width - sparkline_max_width))
|
||||
// nexthop_count_sparkline_data.shift();
|
||||
nexthop_count_sparkline_data.splice(0, (sparkline_data_width - sparkline_max_width))
|
||||
// avg_as_path_length_data.shift();
|
||||
avg_as_path_length_data.splice(0, (sparkline_data_width - sparkline_max_width))
|
||||
// customer_count_data.shift();
|
||||
customer_count_data.splice(0, (sparkline_data_width - sparkline_max_width))
|
||||
// customer_ipv4_prefix_data.shift();
|
||||
customer_ipv4_prefix_data.splice(0, (sparkline_data_width - sparkline_max_width))
|
||||
// customer_ipv6_prefix_data.shift();
|
||||
customer_ipv6_prefix_data.splice(0, (sparkline_data_width - sparkline_max_width))
|
||||
}
|
||||
update_sparklines(data, 1)
|
||||
peers_table.ajax.reload(null, false)
|
||||
});
|
||||
}
|
||||
|
||||
jQuery(document).ready(update_counters());
|
||||
setInterval(function() { update_counters() }, 20000);
|
||||
|
||||
</script>
|
||||
|
||||
<!-- morris.js -->
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
Morris.Bar({
|
||||
element: 'graph_bar',
|
||||
data: [
|
||||
{%- for cidr in cidr_breakdown -%}
|
||||
{% if cidr.ip_version == 4 %}
|
||||
{cidr: "/{{ cidr.mask }}", count: {{ cidr.count }}},
|
||||
{% endif %}
|
||||
{%- endfor -%}
|
||||
],
|
||||
xkey: 'cidr',
|
||||
ykeys: ['count'],
|
||||
labels: ['count'],
|
||||
barRatio: 0.4,
|
||||
barColors: ['#26B99A', '#34495E', '#ACADAC', '#3498DB'],
|
||||
xLabelAngle: 60,
|
||||
hideHover: 'auto',
|
||||
resize: true
|
||||
});
|
||||
|
||||
Morris.Bar({
|
||||
element: 'graph_bar_group',
|
||||
data: [
|
||||
{%- for cidr in cidr_breakdown -%}
|
||||
{% if cidr.ip_version == 6 %}
|
||||
{cidr: "/{{ cidr.mask }}", count: {{ cidr.count }}},
|
||||
{% endif %}
|
||||
{%- endfor -%}
|
||||
],
|
||||
xkey: 'cidr',
|
||||
ykeys: ['count'],
|
||||
labels: ['count'],
|
||||
barRatio: 0.4,
|
||||
barColors: ['#26B99A', '#34495E', '#ACADAC', '#3498DB'],
|
||||
xLabelAngle: 60,
|
||||
hideHover: 'auto',
|
||||
resize: true
|
||||
});
|
||||
|
||||
Morris.Donut({
|
||||
element: 'graph_donut1',
|
||||
data: [
|
||||
{%- for comm in communities -%}
|
||||
{% if comm.community.startswith(peer_bgp_community) %}
|
||||
{label: "{{ comm.name }}", value: {{ comm.count }}},
|
||||
{% endif %}
|
||||
{%- endfor -%}
|
||||
],
|
||||
colors: ['#26B99A', '#34495E', '#E59443', '#3498DB', '#E74C3C', '#FF7BAC', '#26B99A', '#34495E', '#E59443', '#3498DB', '#E74C3C', '#333333'],
|
||||
formatter: function (y) {
|
||||
return y;
|
||||
},
|
||||
resize: true
|
||||
});
|
||||
|
||||
Morris.Donut({
|
||||
element: 'graph_donut2',
|
||||
data: [
|
||||
{% set total_prefixes = data.ipv4_table_size + data.ipv6_table_size %}
|
||||
{%- for comm in communities -%}
|
||||
{% if comm.community == transit_bgp_community %}
|
||||
{label: 'Peering', value: {{ (((comm.count - total_prefixes) / total_prefixes) * -100)|round }}},
|
||||
{label: 'Transit', value: {{ ((comm.count / total_prefixes) * 100)|round }}},
|
||||
{% endif %}
|
||||
{%- endfor -%}
|
||||
],
|
||||
colors: ['#34495E', '#3498DB'],
|
||||
formatter: function (y) {
|
||||
return y + "%";
|
||||
},
|
||||
resize: true
|
||||
});
|
||||
|
||||
// Morris.Donut({
|
||||
// element: 'graph_donut3',
|
||||
// data: [
|
||||
// {%- for comm in communities -%}
|
||||
// {% if comm.community == "3701:370" %}
|
||||
// {label: 'Customer', value: {{ comm.count }}}
|
||||
// {% endif %}
|
||||
// {%- endfor -%}
|
||||
// ],
|
||||
// colors: ['#34495E'],
|
||||
// formatter: function (y) {
|
||||
// return y;
|
||||
// },
|
||||
// resize: true
|
||||
// });
|
||||
|
||||
});
|
||||
</script>
|
||||
<!-- /morris.js -->
|
||||
|
||||
</body>
|
||||
</html>
|
16
flask/app/uwsgi.ini
Normal file
16
flask/app/uwsgi.ini
Normal file
@@ -0,0 +1,16 @@
|
||||
[uwsgi]
|
||||
#application's base folder
|
||||
base = /var/www/app
|
||||
#python module to import
|
||||
module = bgp
|
||||
#the variable that holds a flask application inside the module imported at line #6
|
||||
callable = app
|
||||
#socket file's location
|
||||
socket = /var/www/app/uwsgi.sock
|
||||
#permissions for the socket file
|
||||
chmod-socket = 666
|
||||
#Log directory
|
||||
logto = /var/log/uwsgi/app/app.log
|
||||
enable-threads = true
|
||||
|
||||
chdir = /var/www/app
|
18
flask/nginx/flask.conf
Normal file
18
flask/nginx/flask.conf
Normal file
@@ -0,0 +1,18 @@
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
charset utf-8;
|
||||
client_max_body_size 75M;
|
||||
|
||||
|
||||
location / {
|
||||
include uwsgi_params;
|
||||
uwsgi_pass unix:/var/www/app/uwsgi.sock;
|
||||
}
|
||||
|
||||
location /static {
|
||||
root /var/www/app/;
|
||||
}
|
||||
}
|
||||
|
8
flask/supervisor/supervisord.conf
Normal file
8
flask/supervisor/supervisord.conf
Normal file
@@ -0,0 +1,8 @@
|
||||
[supervisord]
|
||||
nodaemon = true
|
||||
|
||||
[program:nginx]
|
||||
command = /usr/sbin/nginx
|
||||
|
||||
[program:uwsgi]
|
||||
command = /venv/bin/uwsgi --ini /var/www/app/uwsgi.ini
|
26
gobgp/Dockerfile
Executable file
26
gobgp/Dockerfile
Executable file
@@ -0,0 +1,26 @@
|
||||
FROM ubuntu:24.04
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get remove -y binutils && \
|
||||
apt-get install -y \
|
||||
python3 \
|
||||
python3-pip \
|
||||
python3-venv \
|
||||
wget && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
COPY ./requirements.txt /tmp/requirements.txt
|
||||
RUN python3 -m venv venv && chmod +x venv
|
||||
RUN . venv/bin/activate && pip3 install -r /tmp/requirements.txt
|
||||
RUN wget https://github.com/osrg/gobgp/releases/download/v3.27.0/gobgp_3.27.0_linux_amd64.tar.gz && \
|
||||
tar -xzvf gobgp_3.27.0_linux_amd64.tar.gz && \
|
||||
mv gobgp /usr/local/bin/gobgp && \
|
||||
mv gobgpd /usr/local/bin/gobgpd && \
|
||||
rm gobgp_3.27.0_linux_amd64.tar.gz
|
||||
COPY ./gobgpd.conf /root/gobgp/gobgpd.conf
|
||||
COPY ./entrypoint.sh /root/gobgp/entrypoint.sh
|
||||
COPY ./startup.sh /root/gobgp/startup.sh
|
||||
RUN chmod +x /root/gobgp/entrypoint.sh && \
|
||||
chmod +x /root/gobgp/startup.sh
|
||||
ENTRYPOINT ["/root/gobgp/entrypoint.sh"]
|
3
gobgp/entrypoint.sh
Normal file
3
gobgp/entrypoint.sh
Normal file
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
exec gobgpd -f /root/gobgp/gobgpd.conf &
|
||||
exec /root/gobgp/startup.sh
|
15
gobgp/gobgpd.conf
Executable file
15
gobgp/gobgpd.conf
Executable file
@@ -0,0 +1,15 @@
|
||||
[global.config]
|
||||
as = 400848
|
||||
router-id = "172.16.1.66"
|
||||
|
||||
[[neighbors]]
|
||||
[neighbors.config]
|
||||
neighbor-address = "172.16.1.27"
|
||||
peer-as = 400848
|
||||
route-flap-damping = "True"
|
||||
[[neighbors.afi-safis]]
|
||||
[neighbors.afi-safis.config]
|
||||
afi-safi-name = "ipv4-unicast"
|
||||
[[neighbors.afi-safis]]
|
||||
[neighbors.afi-safis.config]
|
||||
afi-safi-name = "ipv6-unicast"
|
3
gobgp/requirements.txt
Normal file
3
gobgp/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
pymongo >= 4.8.0
|
||||
dnspython >= 2.6.1
|
||||
ipaddress >= 1.0.23
|
7
gobgp/startup.sh
Normal file
7
gobgp/startup.sh
Normal file
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
## Production
|
||||
. venv/bin/activate
|
||||
gobgp monitor global rib -j | /var/tmp/gobgp_to_mongo.py
|
||||
##
|
||||
## Dev Test
|
||||
# cat /var/tmp/log/bgp.dump.json | /var/tmp/gobgp_to_mongo.py
|
200
gobgp_to_mongo.py
Executable file
200
gobgp_to_mongo.py
Executable file
@@ -0,0 +1,200 @@
|
||||
#! /usr/bin/env python3
|
||||
|
||||
import sys
|
||||
import json
|
||||
|
||||
from pymongo.database import Database
|
||||
|
||||
import bgp_attributes as BGP
|
||||
from pymongo import MongoClient
|
||||
import pymongo
|
||||
from copy import copy
|
||||
from datetime import datetime
|
||||
import ipaddress
|
||||
import logging
|
||||
|
||||
# logging.basicConfig(level=logging.CRITICAL)
|
||||
# logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
# DEFAULTS - UPDATE ACCORDINGLY
|
||||
MAX_PREFIX_HISTORY = 100 # None = unlimited (BGP flapping will likely kill DB if unlimited)
|
||||
|
||||
|
||||
def db_connect(host='mongodb') -> Database:
|
||||
"""Return a connection to the Mongo Database."""
|
||||
client = MongoClient(host=host)
|
||||
return client['bgp']
|
||||
|
||||
|
||||
def initialize_database(db: Database):
|
||||
"""Create indexes, and if the db contains any entries set them all to 'active': False"""
|
||||
# db['bgp'].drop()
|
||||
db['bgp'].create_index('nexthop')
|
||||
db['bgp'].create_index('nexthop_asn')
|
||||
db['bgp'].create_index([('nexthop', pymongo.ASCENDING), ('active', pymongo.ASCENDING)])
|
||||
db['bgp'].create_index([('nexthop_asn', pymongo.ASCENDING), ('active', pymongo.ASCENDING)])
|
||||
db['bgp'].create_index([('ip_version', pymongo.ASCENDING), ('active', pymongo.ASCENDING)])
|
||||
db['bgp'].create_index(
|
||||
[('origin_asn', pymongo.ASCENDING), ('ip_version', pymongo.ASCENDING), ('active', pymongo.ASCENDING)])
|
||||
db['bgp'].create_index([('communities', pymongo.ASCENDING), ('active', pymongo.ASCENDING)])
|
||||
db['bgp'].create_index(
|
||||
[('as_path.1', pymongo.ASCENDING), ('nexthop_asn', pymongo.ASCENDING), ('active', pymongo.ASCENDING)])
|
||||
db['bgp'].update_many(
|
||||
{"active": True}, # Search for
|
||||
{"$set": {"active": False}}) # Replace with
|
||||
|
||||
|
||||
def get_update_entry(line):
|
||||
"""Read output from GoBGP from stdin and return a update entry *dict*"""
|
||||
try:
|
||||
update_list = json.loads(line)
|
||||
for update_entry in update_list:
|
||||
if 'error' in update_entry:
|
||||
return None
|
||||
else:
|
||||
return update_entry
|
||||
except Exception as err:
|
||||
logging.error("Error in get_update_entry(line):", err)
|
||||
return None
|
||||
|
||||
|
||||
def compare_prefixes(new, old):
|
||||
"""ignore history, age, and active state, then compare prefix objects"""
|
||||
new['history'] = new['age'] = new['active'] = None
|
||||
old['history'] = old['age'] = old['active'] = None
|
||||
if new == old:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def community_32bit_to_string(number):
|
||||
"""Given a 32bit number, convert to standard bgp community format XXX:XX"""
|
||||
if number != 0:
|
||||
return f'{int(bin(number)[:-16], 2)}:{int(bin(number)[-16:], 2)}' # PEP 498
|
||||
|
||||
|
||||
def community_large_to_string(community: dict):
|
||||
"""Given a dict, convert to large bgp community format XXX:XXX:XXX"""
|
||||
return f"{community['ASN']}:{community['LocalData1']}:{community['LocalData2']}"
|
||||
|
||||
|
||||
def build_json(update_entry):
|
||||
"""Given an update entry from GoBGP, set the BGP attribue types as a
|
||||
key/value dict and return"""
|
||||
update_json = { # set defaults
|
||||
'_id': update_entry['nlri']['prefix'],
|
||||
'ip_version': ipaddress.ip_address(update_entry['nlri']['prefix'].split('/', 1)[0]).version,
|
||||
'origin_asn': None,
|
||||
'nexthop': None,
|
||||
'nexthop_asn': None,
|
||||
'as_path': [],
|
||||
'med': 0,
|
||||
'local_pref': 0,
|
||||
'communities': [],
|
||||
'route_origin': None,
|
||||
'atomic_aggregate': None,
|
||||
'aggregator_as': None,
|
||||
'aggregator_address': None,
|
||||
'originator_id': None,
|
||||
'cluster_list': [],
|
||||
'withdrawal': False,
|
||||
'age': 0,
|
||||
'active': True,
|
||||
'history': []
|
||||
}
|
||||
for attribute in update_entry['attrs']:
|
||||
if attribute['type'] == BGP.ORIGIN:
|
||||
update_json['route_origin'] = BGP.ORIGIN_CODE[attribute['value']]
|
||||
if attribute['type'] == BGP.AS_PATH:
|
||||
try:
|
||||
update_json['as_path'] = attribute['as_paths'][0]['asns']
|
||||
update_json['nexthop_asn'] = update_json['as_path'][0]
|
||||
update_json['origin_asn'] = update_json['as_path'][-1]
|
||||
except Exception:
|
||||
logging.debug(f'Error processing as_path: {attribute}')
|
||||
logging.debug(f'Error processing as_path: {update_json["_id"]}')
|
||||
if attribute['type'] == BGP.NEXT_HOP:
|
||||
update_json['nexthop'] = attribute['nexthop']
|
||||
if attribute['type'] == BGP.MULTI_EXIT_DISC:
|
||||
try:
|
||||
update_json['med'] = attribute['metric']
|
||||
except Exception:
|
||||
logging.debug(f'Error processing med: {attribute}')
|
||||
if attribute['type'] == BGP.LOCAL_PREF:
|
||||
try:
|
||||
update_json['local_pref'] = attribute['value']
|
||||
except Exception:
|
||||
logging.debug(f'Error processing local_pref: {attribute}')
|
||||
if attribute['type'] == BGP.ATOMIC_AGGREGATE:
|
||||
update_json['atomic_aggregate'] = True
|
||||
if attribute['type'] == BGP.AGGREGATOR:
|
||||
update_json['aggregator_as'] = attribute['as']
|
||||
update_json['aggregator_address'] = attribute['address']
|
||||
if attribute['type'] == BGP.COMMUNITY:
|
||||
try:
|
||||
for number in attribute['communities']:
|
||||
update_json['communities'].append(community_32bit_to_string(number))
|
||||
except Exception:
|
||||
logging.debug(f'Error processing communities: {attribute}')
|
||||
if attribute['type'] == BGP.ORIGINATOR_ID:
|
||||
update_json['originator_id'] = attribute['value']
|
||||
if attribute['type'] == BGP.CLUSTER_LIST:
|
||||
update_json['cluster_list'] = attribute['value']
|
||||
if attribute['type'] == BGP.MP_REACH_NLRI:
|
||||
update_json['nexthop'] = attribute['nexthop']
|
||||
if attribute['type'] == BGP.MP_UNREACH_NLRI:
|
||||
logging.debug(f'Found MP_UNREACH_NLRI: {attribute}')
|
||||
if attribute['type'] == BGP.EXTENDED_COMMUNITIES:
|
||||
logging.debug(f'Found EXTENDED_COMMUNITIES: {attribute}')
|
||||
if attribute['type'] == BGP.LARGE_COMMUNITIES:
|
||||
try:
|
||||
for community in attribute['value']:
|
||||
update_json['communities'].append(community_large_to_string(community))
|
||||
except Exception:
|
||||
logging.debug(f'Error processing LARGE_COMMUNITIES: {attribute}')
|
||||
if 'withdrawal' in update_entry:
|
||||
update_json['withdrawal'] = update_entry['withdrawal']
|
||||
update_json['active'] = False
|
||||
if 'age' in update_entry:
|
||||
update_json['age'] = datetime.fromtimestamp(update_entry['age']).strftime('%Y-%m-%d %H:%M:%S ') + 'UTC'
|
||||
|
||||
return update_json
|
||||
|
||||
|
||||
def update_prefix(prefix_from_gobgp, prefix_from_database):
|
||||
if compare_prefixes(copy(prefix_from_gobgp), copy(prefix_from_database)):
|
||||
prefix_from_gobgp['active'] = True # flip the active state to true
|
||||
else: # diff between prefix_from_gobgp and prefix_from_database: update history
|
||||
history_list = prefix_from_database['history']
|
||||
del prefix_from_database['active'] # delete house keeping keys from history objects
|
||||
del prefix_from_database['history']
|
||||
if not history_list: # no history: create some
|
||||
prefix_from_gobgp['history'].append(prefix_from_database)
|
||||
else: # existing history: append to history list
|
||||
history_list.insert(0, prefix_from_database) # insert on top of list, index 0
|
||||
prefix_from_gobgp['history'] = history_list[:MAX_PREFIX_HISTORY] # trim the history list if MAX is set
|
||||
return prefix_from_gobgp
|
||||
|
||||
|
||||
def main():
|
||||
db = db_connect()
|
||||
initialize_database(db)
|
||||
for line in sys.stdin:
|
||||
try:
|
||||
prefix_from_gobgp = build_json(get_update_entry(line))
|
||||
|
||||
prefix_from_database = db['bgp'].find_one({'_id': prefix_from_gobgp['_id']})
|
||||
|
||||
if prefix_from_database:
|
||||
updated_prefix = update_prefix(prefix_from_gobgp, prefix_from_database)
|
||||
db['bgp'].update_one({"_id": prefix_from_database['_id']}, {'$set': updated_prefix}, upsert=True)
|
||||
else:
|
||||
db['bgp'].update_one({"_id": prefix_from_gobgp['_id']}, {'$set': prefix_from_gobgp}, upsert=True)
|
||||
|
||||
except TypeError as e:
|
||||
print(f"TypeError {e} in Line: {line}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
Reference in New Issue
Block a user