changes
This commit is contained in:
commit
5e31dd0214
37 changed files with 2082 additions and 0 deletions
12
.env
Normal file
12
.env
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
HOSTNAME=router.something.hi
|
||||
|
||||
VYOS_API_URL=https://vyos-api:port
|
||||
VYOS_API_KEY=vyos-api-key
|
||||
|
||||
BGP_TOOLS_IMAGE=bgp-tools-image
|
||||
|
||||
AKVORADO_BASE_URL=http://ip:port/api/v0/console/widget
|
||||
|
||||
LIBRENMS_URL=https://librenms-url
|
||||
LIBRENMS_PORTS=portname:librenmsid,portname2:librenmsid2
|
||||
LIBRENMS_MAIN_PORT=eth0
|
||||
23
README.md
Normal file
23
README.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# AS215085.net Router tools
|
||||
|
||||
### Vyos config
|
||||
1. Login to Vyos and enter configure mode.
|
||||
2. ```set service https api keys id api key 'YourKey'```
|
||||
3. ```set service https listen-address 'VyosIP'```
|
||||
4. ```set service https port '80'```
|
||||
5. ```set service https allow-client address '0.0.0.0/0'```
|
||||
|
||||
### App config
|
||||
1. Edit .env and fill in the details
|
||||
2. ```apt install -y python3 python3-jinja2 python3-flask python3-gunicorn```
|
||||
3. ```gunicorn -w 4 -b 0.0.0.0:5000 app:app```
|
||||
|
||||
## License
|
||||
|
||||
```This project is licensed under the Apache License 2.0 with the Commons Clause restriction.```
|
||||
|
||||
```You may use, modify, and distribute this work for personal and non-commercial purposes only.```
|
||||
|
||||
**You must provide a clear link or reference to the original project when redistributing or using this work.**
|
||||
|
||||
```Commercial use is strictly prohibited.```
|
||||
170
app.py
Normal file
170
app.py
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
import json
|
||||
import subprocess
|
||||
import os
|
||||
import requests
|
||||
from jinja2 import Template
|
||||
from flask import Flask, render_template_string, jsonify, url_for, redirect, render_template, request, abort
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
from modules.parse import run_bgp_curl_command, run_arp_curl_command, run_neighbors_curl_command, run_interfaces_curl_command, run_bgp_route_curl_command
|
||||
|
||||
from modules.bgp import parse_bgp_data, generate_bgp_json
|
||||
|
||||
from modules.arp import parse_arp_data, generate_arp_json
|
||||
|
||||
from modules.neighbors import parse_neighbors_data, generate_neighbors_json
|
||||
|
||||
from modules.interfaces import parse_interface_data
|
||||
|
||||
from modules.akvorado import get_widget_data
|
||||
|
||||
from modules.librenms import get_port_id, fetch_graph_base64
|
||||
|
||||
@app.context_processor
|
||||
def inject_hostname():
|
||||
return dict(hostname=os.getenv("HOSTNAME", "unknown"))
|
||||
|
||||
@app.route("/ping")
|
||||
def ping():
|
||||
return "pong"
|
||||
|
||||
@app.errorhandler(404)
|
||||
def page_not_found(e):
|
||||
return render_template("404.html"), 404
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
bgp_image = os.getenv("BGP_TOOLS_IMAGE")
|
||||
return render_template('index.html', bgp_image=bgp_image)
|
||||
|
||||
@app.route("/bgp")
|
||||
def bgp():
|
||||
bgp_data = run_bgp_curl_command()
|
||||
ipv4_info, ipv4_peers, ipv6_info, ipv6_peers = parse_bgp_data(bgp_data)
|
||||
return render_template('bgp.html')
|
||||
|
||||
@app.route("/bgp/json")
|
||||
def bgp_json():
|
||||
bgp_data = run_bgp_curl_command()
|
||||
ipv4_info, ipv4_peers, ipv6_info, ipv6_peers = parse_bgp_data(bgp_data)
|
||||
return jsonify(generate_bgp_json(ipv4_info, ipv4_peers, ipv6_info, ipv6_peers))
|
||||
|
||||
@app.route("/arp")
|
||||
def arp():
|
||||
return render_template("arp.html")
|
||||
|
||||
@app.route("/arp/json")
|
||||
def arp_json():
|
||||
arp_data = run_arp_curl_command()
|
||||
arp_table = parse_arp_data(arp_data)
|
||||
return jsonify(generate_arp_json(arp_table))
|
||||
|
||||
@app.route("/neighbors")
|
||||
def neighbors():
|
||||
return render_template("neighbors.html")
|
||||
|
||||
@app.route("/neighbors/json")
|
||||
def neighborsp_json():
|
||||
neighbors_data = run_neighbors_curl_command()
|
||||
neighbors_table = parse_neighbors_data(neighbors_data)
|
||||
return jsonify(generate_neighbors_json(neighbors_table))
|
||||
|
||||
@app.route('/interfaces')
|
||||
def interface_table_page():
|
||||
return render_template("interfaces.html")
|
||||
|
||||
@app.route('/interfaces/json')
|
||||
def interface_table_summary_json():
|
||||
data = run_interfaces_curl_command()
|
||||
interface_table = parse_interface_data(data)
|
||||
return jsonify({"interface_table": interface_table})
|
||||
|
||||
@app.route('/bgp-route')
|
||||
def bgp_route_page():
|
||||
return render_template("bgp-route.html")
|
||||
|
||||
@app.route('/bgp-route/lookup', methods=['POST'])
|
||||
def bgp_route_lookup():
|
||||
data = request.json
|
||||
ip_version = data.get('ip_version')
|
||||
bgprouteprefix = data.get('bgprouteprefix')
|
||||
|
||||
if not ip_version or not bgprouteprefix:
|
||||
return jsonify({"error": "ip_version and bgprouteprefix are required"}), 400
|
||||
|
||||
try:
|
||||
result = run_bgp_route_curl_command(ip_version, bgprouteprefix)
|
||||
return jsonify(result)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/stats')
|
||||
def stats_page():
|
||||
interface_name = os.getenv("LIBRENMS_MAIN_PORT", "dummy")
|
||||
port_id = get_port_id(interface_name)
|
||||
if not port_id:
|
||||
return render_template("stats.html", interface_name=interface_name, error=True)
|
||||
|
||||
daily = fetch_graph_base64(port_id)
|
||||
return render_template(
|
||||
"stats.html",
|
||||
interface_name=interface_name,
|
||||
daily=daily,
|
||||
error=False
|
||||
)
|
||||
|
||||
@app.route("/stats/src-as")
|
||||
def stats_srcas_json():
|
||||
return jsonify(get_widget_data("top/src-as"))
|
||||
|
||||
@app.route("/stats/src-ports")
|
||||
def stats_srcport_json():
|
||||
return jsonify(get_widget_data("top/src-port"))
|
||||
|
||||
@app.route("/stats/protocol")
|
||||
def stats_protocol_json():
|
||||
return jsonify(get_widget_data("top/protocol"))
|
||||
|
||||
@app.route("/stats/src-country")
|
||||
def stats_srccountry_json():
|
||||
return jsonify(get_widget_data("top/src-country"))
|
||||
|
||||
@app.route("/stats/etype")
|
||||
def stats_etype_json():
|
||||
return jsonify(get_widget_data("top/etype"))
|
||||
|
||||
@app.route("/stats/graph")
|
||||
def stats_graph_json():
|
||||
return jsonify(get_widget_data("graph"))
|
||||
|
||||
@app.route("/stats/flow-rate")
|
||||
def stats_flow_rate_json():
|
||||
return jsonify(get_widget_data("flow-rate"))
|
||||
|
||||
@app.route("/stats/exporters")
|
||||
def stats_exporters_json():
|
||||
return jsonify(get_widget_data("exporters"))
|
||||
|
||||
@app.route("/port/<interface_name>")
|
||||
def graph_page(interface_name):
|
||||
port_id = get_port_id(interface_name)
|
||||
if not port_id:
|
||||
return render_template(
|
||||
"port.html",
|
||||
interface_name=interface_name,
|
||||
error=True
|
||||
)
|
||||
|
||||
daily = fetch_graph_base64(port_id)
|
||||
weekly = fetch_graph_base64(port_id, days_ago=7)
|
||||
monthly = fetch_graph_base64(port_id, days_ago=28)
|
||||
|
||||
return render_template(
|
||||
"port.html",
|
||||
interface_name=interface_name,
|
||||
daily=daily,
|
||||
weekly=weekly,
|
||||
monthly=monthly,
|
||||
error=False
|
||||
)
|
||||
0
modules/__init__.py
Normal file
0
modules/__init__.py
Normal file
17
modules/akvorado.py
Normal file
17
modules/akvorado.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import os
|
||||
import requests
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
BASE_URL = os.getenv("AKVORADO_BASE_URL", "http://localhost:8081/api/v0/console/widget")
|
||||
|
||||
def get_widget_data(endpoint: str):
|
||||
url = f"{BASE_URL}/{endpoint}?0"
|
||||
try:
|
||||
res = requests.get(url)
|
||||
res.raise_for_status()
|
||||
return res.json()
|
||||
except requests.RequestException as e:
|
||||
print(f"Fout bij ophalen van {endpoint}: {e}")
|
||||
return {"top": []}
|
||||
21
modules/arp.py
Normal file
21
modules/arp.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
def parse_arp_data(data):
|
||||
arp_table = []
|
||||
|
||||
if "data" in data:
|
||||
raw_data = data["data"]
|
||||
|
||||
for line in raw_data.split("\n"):
|
||||
if line.strip() and not line.startswith("Address") and "---" not in line:
|
||||
arp_info = line.split()
|
||||
if len(arp_info) >= 4:
|
||||
arp_table.append({
|
||||
"address": arp_info[0],
|
||||
"interface": arp_info[1],
|
||||
"link_layer_address": arp_info[2],
|
||||
"state": arp_info[3]
|
||||
})
|
||||
|
||||
return arp_table
|
||||
|
||||
def generate_arp_json(arp_table):
|
||||
return {"arp_table": arp_table}
|
||||
84
modules/bgp.py
Normal file
84
modules/bgp.py
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
def parse_bgp_data(data):
|
||||
ipv4_section = ""
|
||||
ipv6_section = ""
|
||||
ipv4_info = {}
|
||||
ipv6_info = {}
|
||||
|
||||
if "data" in data:
|
||||
raw_data = data["data"]
|
||||
|
||||
ipv4_marker = "IPv4 Unicast Summary (VRF bgp):"
|
||||
ipv6_marker = "IPv6 Unicast Summary (VRF bgp):"
|
||||
|
||||
ipv4_start = raw_data.find(ipv4_marker)
|
||||
ipv6_start = raw_data.find(ipv6_marker)
|
||||
|
||||
if ipv4_start != -1:
|
||||
if ipv6_start != -1:
|
||||
ipv4_section = raw_data[ipv4_start + len(ipv4_marker):ipv6_start].strip()
|
||||
else:
|
||||
ipv4_section = raw_data[ipv4_start + len(ipv4_marker):].strip()
|
||||
ipv4_info = extract_bgp_info(ipv4_section)
|
||||
|
||||
if ipv6_start != -1:
|
||||
ipv6_section = raw_data[ipv6_start + len(ipv6_marker):].strip()
|
||||
ipv6_info = extract_bgp_info(ipv6_section)
|
||||
|
||||
def process_peers(peer_data):
|
||||
peers = []
|
||||
for line in peer_data.split("\n"):
|
||||
if line.strip().startswith("Neighbor"):
|
||||
continue
|
||||
if line.strip():
|
||||
peer_info = line.split()
|
||||
if len(peer_info) >= 12:
|
||||
peers.append({
|
||||
"neighbor": peer_info[0],
|
||||
"version": peer_info[1],
|
||||
"as_number": peer_info[2],
|
||||
"msg_received": peer_info[3],
|
||||
"msg_sent": peer_info[4],
|
||||
"table_version": peer_info[5],
|
||||
"in_queue": peer_info[6],
|
||||
"out_queue": peer_info[7],
|
||||
"up_down": peer_info[8],
|
||||
"state_pfx_rcd": peer_info[9],
|
||||
"prefix_sent": peer_info[10],
|
||||
"description": " ".join(peer_info[11:])
|
||||
})
|
||||
return peers
|
||||
|
||||
ipv4_peers = process_peers(ipv4_section)
|
||||
ipv6_peers = process_peers(ipv6_section)
|
||||
|
||||
return ipv4_info, ipv4_peers, ipv6_info, ipv6_peers
|
||||
|
||||
def extract_bgp_info(raw_data):
|
||||
lines = raw_data.split("\n")
|
||||
info = {}
|
||||
for line in lines:
|
||||
if "BGP router identifier" in line:
|
||||
parts = line.split(",")
|
||||
info["router_id"] = parts[0].split("identifier")[1].strip()
|
||||
info["local_as"] = parts[1].split("number")[1].strip().split(" ")[0]
|
||||
if "vrf-id" in parts[-1]:
|
||||
info["vrf_id"] = parts[-1].split("vrf-id")[1].strip()
|
||||
if "BGP table version" in line:
|
||||
info["table_version"] = line.split("version")[1].strip()
|
||||
if "RIB entries" in line:
|
||||
parts = line.split(",")
|
||||
info["rib_entries"] = parts[0].split("entries")[1].strip()
|
||||
info["rib_memory"] = parts[1].split("using")[1].strip()
|
||||
if "Peers" in line:
|
||||
parts = line.split(",")
|
||||
info["peers"] = parts[0].split("Peers")[1].strip()
|
||||
info["peers_memory"] = parts[1].split("using")[1].strip()
|
||||
return info
|
||||
|
||||
def generate_bgp_json(ipv4_info, ipv4_peers, ipv6_info, ipv6_peers):
|
||||
return {
|
||||
"ipv4_info": ipv4_info,
|
||||
"ipv4_peers": ipv4_peers,
|
||||
"ipv6_info": ipv6_info,
|
||||
"ipv6_peers": ipv6_peers
|
||||
}
|
||||
30
modules/interfaces.py
Normal file
30
modules/interfaces.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
def parse_interface_data(data):
|
||||
interface_table = []
|
||||
|
||||
if "data" in data:
|
||||
raw_data = data["data"]
|
||||
|
||||
for line in raw_data.split("\n"):
|
||||
if line.startswith("Interface") and "IP Address" in line:
|
||||
continue
|
||||
|
||||
if line.startswith("Codes:"):
|
||||
continue
|
||||
|
||||
if line.strip().startswith('-'):
|
||||
continue
|
||||
|
||||
if line.strip():
|
||||
interface_info = line.split()
|
||||
if len(interface_info) >= 6:
|
||||
interface_table.append({
|
||||
"interface": interface_info[0],
|
||||
"ip_address": interface_info[1] if interface_info[1] != '-' else 'N/A',
|
||||
"mac_address": interface_info[2],
|
||||
"vrf": interface_info[3],
|
||||
"mtu": interface_info[4],
|
||||
"status": interface_info[5],
|
||||
"description": " ".join(interface_info[6:])
|
||||
})
|
||||
|
||||
return interface_table
|
||||
43
modules/librenms.py
Normal file
43
modules/librenms.py
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import os
|
||||
import time
|
||||
import base64
|
||||
import requests
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
LIBRENMS_URL = os.getenv("LIBRENMS_URL", "https://nms.pixelhosting.nl")
|
||||
|
||||
_ports_cache = None
|
||||
|
||||
def get_librenms_ports():
|
||||
global _ports_cache
|
||||
if _ports_cache is None:
|
||||
ports_str = os.getenv("LIBRENMS_PORTS", "")
|
||||
ports = {}
|
||||
if ports_str:
|
||||
pairs = ports_str.split(",")
|
||||
for pair in pairs:
|
||||
if ":" in pair:
|
||||
key, val = pair.split(":", 1)
|
||||
ports[key.strip()] = val.strip()
|
||||
_ports_cache = ports
|
||||
return _ports_cache
|
||||
|
||||
def get_port_id(interface_name):
|
||||
return get_librenms_ports().get(interface_name)
|
||||
|
||||
def get_timestamp_days_ago(days):
|
||||
return int(time.time()) - (days * 86400)
|
||||
|
||||
def fetch_graph_base64(port_id, days_ago=None):
|
||||
url = f"{LIBRENMS_URL}/graph.php?id={port_id}&type=port_bits&height=200&width=500"
|
||||
if days_ago:
|
||||
url += f"&from={get_timestamp_days_ago(days_ago)}"
|
||||
try:
|
||||
response = requests.get(url, timeout=5)
|
||||
response.raise_for_status()
|
||||
return base64.b64encode(response.content).decode("utf-8")
|
||||
except Exception as e:
|
||||
print(f"[LibreNMS] Error fetching graph: {e}")
|
||||
return None
|
||||
21
modules/neighbors.py
Normal file
21
modules/neighbors.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
def parse_neighbors_data(data):
|
||||
neighbors_table = []
|
||||
|
||||
if "data" in data:
|
||||
raw_data = data["data"]
|
||||
|
||||
for line in raw_data.split("\n"):
|
||||
if line.strip() and not line.startswith("Address") and "---" not in line:
|
||||
neighbors_info = line.split()
|
||||
if len(neighbors_info) >= 4:
|
||||
neighbors_table.append({
|
||||
"address": neighbors_info[0],
|
||||
"interface": neighbors_info[1],
|
||||
"link_layer_address": neighbors_info[2],
|
||||
"state": neighbors_info[3]
|
||||
})
|
||||
|
||||
return neighbors_table
|
||||
|
||||
def generate_neighbors_json(neighbors_table):
|
||||
return {"neighbors_table": neighbors_table}
|
||||
58
modules/parse.py
Normal file
58
modules/parse.py
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import subprocess
|
||||
import json
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
VYOS_API_URL = os.getenv("VYOS_API_URL")
|
||||
VYOS_API_KEY = os.getenv("VYOS_API_KEY")
|
||||
|
||||
def run_bgp_curl_command():
|
||||
curl_command = [
|
||||
"curl", "-k", "--location", "--request", "POST", f"{VYOS_API_URL}/show",
|
||||
"--form", "data={\"op\": \"show\", \"path\": [\"bgp\", \"vrf\", \"bgp\", \"summ\"]}",
|
||||
"--form", f"key={VYOS_API_KEY}"
|
||||
]
|
||||
response = subprocess.check_output(curl_command, text=True)
|
||||
return json.loads(response)
|
||||
|
||||
def run_arp_curl_command():
|
||||
curl_command = [
|
||||
"curl", "-k", "--location", "--request", "POST", f"{VYOS_API_URL}/show",
|
||||
"--form", "data={\"op\": \"show\", \"path\": [\"arp\"]}",
|
||||
"--form", f"key={VYOS_API_KEY}"
|
||||
]
|
||||
response = subprocess.check_output(curl_command, text=True)
|
||||
return json.loads(response)
|
||||
|
||||
def run_neighbors_curl_command():
|
||||
curl_command = [
|
||||
"curl", "-k", "--location", "--request", "POST", f"{VYOS_API_URL}/show",
|
||||
"--form", "data={\"op\": \"show\", \"path\": [\"ipv6\", \"neighbors\"]}",
|
||||
"--form", f"key={VYOS_API_KEY}"
|
||||
]
|
||||
response = subprocess.check_output(curl_command, text=True)
|
||||
return json.loads(response)
|
||||
|
||||
def run_interfaces_curl_command():
|
||||
curl_command = [
|
||||
"curl", "-k", "--location", "--request", "POST", f"{VYOS_API_URL}/show",
|
||||
"--form", "data={\"op\": \"show\", \"path\": [\"interfaces\"]}",
|
||||
"--form", f"key={VYOS_API_KEY}"
|
||||
]
|
||||
response = subprocess.check_output(curl_command, text=True)
|
||||
return json.loads(response)
|
||||
|
||||
def run_bgp_route_curl_command(ip_version, bgprouteprefix):
|
||||
data_json = {
|
||||
"op": "show",
|
||||
"path": ["bgp", "vrf", "bgp", ip_version, bgprouteprefix]
|
||||
}
|
||||
curl_command = [
|
||||
"curl", "-k", "--location", "--request", "POST", f"{VYOS_API_URL}/show",
|
||||
"--form", f"data={json.dumps(data_json)}",
|
||||
"--form", f"key={VYOS_API_KEY}"
|
||||
]
|
||||
response = subprocess.check_output(curl_command, text=True)
|
||||
return json.loads(response)
|
||||
6
static/css/bootstrap.min.css
vendored
Normal file
6
static/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
248
static/css/style.css
Normal file
248
static/css/style.css
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #f9f9f9;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
header {
|
||||
position: relative;
|
||||
background: url('../img/background.webp') no-repeat center center;
|
||||
background-size: cover;
|
||||
color: white;
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
header::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
header h1, header p {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
margin: 0;
|
||||
font-size: 2.5em;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
position: relative;
|
||||
z-index: 2; /* hoger dan de overlay */
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 20px;
|
||||
max-width: 1440px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
section {
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
section h2 {
|
||||
color: #07AAF9;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
footer {
|
||||
background-color: #2D2E43;
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding: 10px 0;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
gap: 50px;
|
||||
}
|
||||
|
||||
.upstreamcon {
|
||||
border: 2px solid #07AAF9;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
width: 45%;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
table {
|
||||
font-family: arial, sans-serif;
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
td, th {
|
||||
border: 1px solid #dddddd;
|
||||
text-align: left;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
tr:nth-child(even) {
|
||||
background-color: #dddddd;
|
||||
}
|
||||
|
||||
th.sortable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sort-arrow::after {
|
||||
content: "⇅";
|
||||
margin-left: 5px;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
th.asc .sort-arrow::after {
|
||||
content: "▲";
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
th.desc .sort-arrow::after {
|
||||
content: "▼";
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.spinner-border {
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
margin-top: 6rem;
|
||||
margin-bottom: 6rem;
|
||||
border-width: 0.4em;
|
||||
}
|
||||
|
||||
.spinner-border {
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
margin-top: 6rem;
|
||||
margin-bottom: 6rem;
|
||||
border-width: 0.4em;
|
||||
}
|
||||
|
||||
#refreshSpinner.spinner-border,
|
||||
#refreshSpinner2.spinner-border {
|
||||
width: 0.84rem;
|
||||
height: 0.84rem;
|
||||
margin-top: 0rem;
|
||||
margin-bottom: 0rem;
|
||||
border-width: 0.15em;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
|
||||
#bgpOutput {
|
||||
white-space: pre-wrap;
|
||||
background-color: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
padding: 1rem;
|
||||
border-radius: 5px;
|
||||
min-height: 400px;
|
||||
min-width: 800px;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
font-family: Consolas, monospace, monospace;
|
||||
border: 1px solid #444;
|
||||
}
|
||||
.asn-highlight {
|
||||
color: #e67e22;
|
||||
font-weight: bold;
|
||||
}
|
||||
.asn215085-highlight {
|
||||
color: #07AAF9;
|
||||
font-weight: bold;
|
||||
}
|
||||
strong.best-line {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#achart-container {
|
||||
display: flex;
|
||||
gap: 4rem;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
max-width: 100%;
|
||||
}
|
||||
.achart-wrapper {
|
||||
width: 220px;
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
text-align: center;
|
||||
}
|
||||
canvas {
|
||||
max-width: 100%;
|
||||
height: auto !important;
|
||||
}
|
||||
.alegend ul {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 0;
|
||||
margin-top: 1rem;
|
||||
gap: 0.5rem 1rem;
|
||||
list-style: none;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.alegend li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.alegend-color {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-right: 6px;
|
||||
vertical-align: middle;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.achart-wrapper-chart{
|
||||
width: 1300px;
|
||||
height: 600px;
|
||||
position: relative;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.achart-wrapper-chart canvas{
|
||||
width: 100%!important;
|
||||
height: 100%!important;
|
||||
}
|
||||
|
||||
.stats-overview-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
#stats-container {
|
||||
text-align: right;
|
||||
display: flex;
|
||||
gap: 1em;
|
||||
}
|
||||
#stats-container p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#legend1 li.inactive,
|
||||
#legend2 li.inactive,
|
||||
#legend3 li.inactive,
|
||||
#legend4 li.inactive,
|
||||
#legend5 li.inactive {
|
||||
opacity: 0.5;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
BIN
static/img/404.jpg
Normal file
BIN
static/img/404.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 323 KiB |
BIN
static/img/as215085-logo.png
Normal file
BIN
static/img/as215085-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
BIN
static/img/background.webp
Normal file
BIN
static/img/background.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
BIN
static/img/favicon.png
Normal file
BIN
static/img/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.6 KiB |
7
static/js/bootstrap.bundle.min.js
vendored
Normal file
7
static/js/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
14
static/js/chart.js
Normal file
14
static/js/chart.js
Normal file
File diff suppressed because one or more lines are too long
7
static/js/chartjs-adapter-date-fns.js
Normal file
7
static/js/chartjs-adapter-date-fns.js
Normal file
File diff suppressed because one or more lines are too long
6
static/js/materialize.min.js
vendored
Normal file
6
static/js/materialize.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
315
static/js/pages/akvorado.js
Normal file
315
static/js/pages/akvorado.js
Normal file
|
|
@ -0,0 +1,315 @@
|
|||
const chartInstances = {};
|
||||
|
||||
async function loadPieChart(endpoint, canvasId, legendId) {
|
||||
try {
|
||||
const res = await fetch(endpoint);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const json = await res.json();
|
||||
|
||||
const canvas = document.getElementById(canvasId);
|
||||
const legend = document.getElementById(legendId);
|
||||
|
||||
if (!json.top || json.top.length === 0) {
|
||||
legend.innerHTML = "<p>No data received.</p>";
|
||||
return;
|
||||
}
|
||||
|
||||
const totalPercent = json.top.reduce((sum, item) => sum + item.percent, 0);
|
||||
let topData = [...json.top];
|
||||
if (totalPercent < 100) {
|
||||
topData.push({ name: "Other", percent: 100 - totalPercent });
|
||||
}
|
||||
|
||||
const labels = topData.map(item => item.name);
|
||||
const data = topData.map(item => item.percent);
|
||||
const baseColors = labels.map((label, i) =>
|
||||
label === "Other" ? "#888888" : `hsl(${(i * 57) % 360}, 70%, 60%)`
|
||||
);
|
||||
|
||||
if (!chartInstances[canvasId]) {
|
||||
const ctx = canvas.getContext("2d");
|
||||
chartInstances[canvasId] = new Chart(ctx, {
|
||||
type: "pie",
|
||||
data: {
|
||||
labels,
|
||||
datasets: [{
|
||||
data,
|
||||
backgroundColor: baseColors,
|
||||
borderColor: "#fff",
|
||||
borderWidth: 2,
|
||||
hoverOffset: 15
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: {
|
||||
enabled: true,
|
||||
callbacks: {
|
||||
label: ctx => `${ctx.label}: ${ctx.parsed.toFixed(2)}%`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
chartInstances[canvasId].hiddenSlices = new Set();
|
||||
} else {
|
||||
const chart = chartInstances[canvasId];
|
||||
chart.data.labels = labels;
|
||||
chart.data.datasets[0].data = data;
|
||||
chart.data.datasets[0].backgroundColor = baseColors;
|
||||
chart.update();
|
||||
}
|
||||
|
||||
const chart = chartInstances[canvasId];
|
||||
|
||||
const hiddenSlices = chart.hiddenSlices;
|
||||
|
||||
legend.innerHTML = "";
|
||||
const ul = document.createElement("ul");
|
||||
ul.style.listStyle = "none";
|
||||
ul.style.padding = "0";
|
||||
ul.style.margin = "0";
|
||||
|
||||
topData.forEach((item, i) => {
|
||||
const li = document.createElement("li");
|
||||
li.style.cursor = "pointer";
|
||||
li.style.display = "flex";
|
||||
li.style.alignItems = "center";
|
||||
li.style.marginBottom = "6px";
|
||||
li.style.userSelect = "none";
|
||||
|
||||
const colorBox = document.createElement("span");
|
||||
colorBox.style.display = "inline-block";
|
||||
colorBox.style.width = "16px";
|
||||
colorBox.style.height = "16px";
|
||||
colorBox.style.marginRight = "8px";
|
||||
colorBox.style.borderRadius = "3px";
|
||||
|
||||
const labelText = item.name === "Other" ? "Other" : item.name.split(":")[0].trim();
|
||||
li.appendChild(colorBox);
|
||||
li.appendChild(document.createTextNode(labelText));
|
||||
|
||||
if (hiddenSlices.has(i)) {
|
||||
colorBox.style.backgroundColor = "#bbb";
|
||||
li.style.opacity = "0.5";
|
||||
} else {
|
||||
colorBox.style.backgroundColor = baseColors[i];
|
||||
li.style.opacity = "1";
|
||||
}
|
||||
|
||||
li.addEventListener("mouseenter", () => {
|
||||
if (hiddenSlices.has(i)) return;
|
||||
chart.setActiveElements([{ datasetIndex: 0, index: i }]);
|
||||
chart.update();
|
||||
});
|
||||
|
||||
li.addEventListener("mouseleave", () => {
|
||||
chart.setActiveElements([]);
|
||||
chart.update();
|
||||
});
|
||||
|
||||
li.addEventListener("click", () => {
|
||||
if (hiddenSlices.has(i)) {
|
||||
hiddenSlices.delete(i);
|
||||
} else {
|
||||
hiddenSlices.add(i);
|
||||
}
|
||||
|
||||
chart.getDatasetMeta(0).data[i].hidden = hiddenSlices.has(i);
|
||||
|
||||
if (hiddenSlices.has(i)) {
|
||||
colorBox.style.backgroundColor = "#bbb";
|
||||
li.style.opacity = "0.5";
|
||||
} else {
|
||||
colorBox.style.backgroundColor = baseColors[i];
|
||||
li.style.opacity = "1";
|
||||
}
|
||||
|
||||
chart.update();
|
||||
});
|
||||
|
||||
ul.appendChild(li);
|
||||
});
|
||||
|
||||
legend.appendChild(ul);
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
document.getElementById(legendId).innerHTML = `<p style="color:red">Error retrieving data: ${err.message}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
function loadAllCharts() {
|
||||
loadPieChart("/stats/src-as", "pieChart1", "legend1");
|
||||
loadPieChart("/stats/src-ports", "pieChart2", "legend2");
|
||||
loadPieChart("/stats/protocol", "pieChart3", "legend3");
|
||||
loadPieChart("/stats/src-country", "pieChart4", "legend4");
|
||||
loadPieChart("/stats/etype", "pieChart5", "legend5");
|
||||
}
|
||||
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
loadAllCharts();
|
||||
setInterval(loadAllCharts, 60000);
|
||||
});
|
||||
|
||||
let chart;
|
||||
|
||||
async function fetchData() {
|
||||
const response = await fetch('/stats/graph');
|
||||
const jsonData = await response.json();
|
||||
|
||||
const rawData = jsonData.data.map(item => {
|
||||
const date = new Date(item.t);
|
||||
return {
|
||||
x: date,
|
||||
y: item.gbps
|
||||
};
|
||||
});
|
||||
|
||||
const maxVal = Math.max(...rawData.map(p => p.y));
|
||||
const scaleFactor = maxVal < 1 ? 1000 : 1;
|
||||
const unitLabel = maxVal < 1 ? 'Mbit/s' : 'Gbit/s';
|
||||
|
||||
const data = rawData.map(p => ({
|
||||
x: p.x,
|
||||
y: p.y * scaleFactor
|
||||
}));
|
||||
|
||||
return { data, unitLabel };
|
||||
}
|
||||
|
||||
async function initChart() {
|
||||
const canvas = document.getElementById('liveChart');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
canvas.width = rect.width * dpr;
|
||||
canvas.height = rect.height * dpr;
|
||||
ctx.scale(dpr, dpr);
|
||||
|
||||
const { data, unitLabel } = await fetchData();
|
||||
|
||||
chart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
datasets: [{
|
||||
label: `Network traffic (${unitLabel})`,
|
||||
data,
|
||||
borderColor: 'blue',
|
||||
backgroundColor: 'rgba(0, 0, 255, 0.1)',
|
||||
fill: true,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 5
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: {
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
type: 'time',
|
||||
time: {
|
||||
unit: 'hour',
|
||||
displayFormats: {
|
||||
hour: 'HH:mm'
|
||||
}
|
||||
},
|
||||
ticks: {
|
||||
autoSkip: false,
|
||||
callback: function(value, index, ticks) {
|
||||
const current = new Date(value);
|
||||
const prev = index > 0 ? new Date(ticks[index - 1].value) : null;
|
||||
|
||||
const hours = current.getHours().toString().padStart(2, '0');
|
||||
const minutes = current.getMinutes().toString().padStart(2, '0');
|
||||
const day = current.getDate().toString().padStart(2, '0');
|
||||
|
||||
const isEvery4Hours = current.getHours() % 4 === 0 && minutes === '00';
|
||||
const isNewDay = !prev || current.getDate() !== prev.getDate();
|
||||
|
||||
if (isNewDay) return `${day}`;
|
||||
if (isEvery4Hours) return `${hours}:${minutes}`;
|
||||
return '';
|
||||
}
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Time'
|
||||
}
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: unitLabel
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
tooltip: {
|
||||
enabled: true,
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
const label = context.dataset.label || '';
|
||||
const value = context.parsed.y;
|
||||
return `${label}: ${value.toFixed(2)}`;
|
||||
}
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
display: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function updateChart() {
|
||||
const { data, unitLabel } = await fetchData();
|
||||
chart.data.datasets[0].data = data;
|
||||
chart.data.datasets[0].label = `Network traffic (${unitLabel})`;
|
||||
chart.options.scales.y.title.text = unitLabel;
|
||||
chart.update();
|
||||
}
|
||||
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
loadAllCharts?.();
|
||||
initChart();
|
||||
setInterval(() => {
|
||||
loadAllCharts?.();
|
||||
updateChart();
|
||||
}, 60000);
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
async function fetchData() {
|
||||
try {
|
||||
const flowResp = await fetch('/stats/flow-rate');
|
||||
const flowData = await flowResp.json();
|
||||
|
||||
const exportersResp = await fetch('/stats/exporters');
|
||||
const exportersData = await exportersResp.json();
|
||||
|
||||
const flowRateRounded = Math.round(flowData.rate);
|
||||
const exportersCount = exportersData.exporters.length;
|
||||
|
||||
document.getElementById('flowRate').textContent = flowRateRounded;
|
||||
document.getElementById('exporterCount').textContent = exportersCount;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching stats:', error);
|
||||
document.getElementById('flowRate').textContent = 'Error';
|
||||
document.getElementById('exporterCount').textContent = 'Error';
|
||||
}
|
||||
}
|
||||
|
||||
fetchData();
|
||||
setInterval(fetchData, 10000);
|
||||
});
|
||||
110
static/js/pages/arp.js
Normal file
110
static/js/pages/arp.js
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
function filterTable(searchInputId,tableId){
|
||||
const input=document.getElementById(searchInputId);
|
||||
const filter=input.value.toUpperCase();
|
||||
const table=document.getElementById(tableId);
|
||||
const tr=table.getElementsByTagName("tr");
|
||||
|
||||
for(let i=1;i<tr.length;i++){
|
||||
tr[i].style.display="none";
|
||||
const td=tr[i].getElementsByTagName("td");
|
||||
for(let j=0;j<td.length;j++){
|
||||
if(td[j]){
|
||||
const txtValue=td[j].textContent||td[j].innerText;
|
||||
if(txtValue.toUpperCase().indexOf(filter)>-1){
|
||||
tr[i].style.display="";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sortTable(tableId,columnIndex,th){
|
||||
const table=document.getElementById(tableId);
|
||||
const tbody=table.tBodies[0];
|
||||
const rows=Array.from(tbody.rows);
|
||||
const headers=Array.from(th.parentNode.children);
|
||||
|
||||
headers.forEach(header=>{
|
||||
if(header!==th){
|
||||
header.classList.remove('asc','desc');
|
||||
header.setAttribute('data-sort-state','none');
|
||||
}
|
||||
});
|
||||
|
||||
let currentState=th.getAttribute('data-sort-state')||'none';
|
||||
let newState=currentState==='none'?'asc':currentState==='asc'?'desc':'none';
|
||||
|
||||
th.classList.remove('asc','desc');
|
||||
if(newState!=='none')th.classList.add(newState);
|
||||
th.setAttribute('data-sort-state',newState);
|
||||
|
||||
if(newState==='none'){
|
||||
rows.sort((a,b)=>parseInt(a.getAttribute('data-index'))-parseInt(b.getAttribute('data-index')));
|
||||
}else{
|
||||
rows.sort((a,b)=>{
|
||||
const aText=a.cells[columnIndex].textContent.trim();
|
||||
const bText=b.cells[columnIndex].textContent.trim();
|
||||
|
||||
const aNum=parseFloat(aText.replace(/[^0-9.-]/g,''));
|
||||
const bNum=parseFloat(bText.replace(/[^0-9.-]/g,''));
|
||||
const isNumeric=!isNaN(aNum)&&!isNaN(bNum);
|
||||
|
||||
return newState==='asc'
|
||||
?(isNumeric?aNum-bNum:aText.localeCompare(bText))
|
||||
:(isNumeric?bNum-aNum:bText.localeCompare(aText));
|
||||
});
|
||||
}
|
||||
|
||||
rows.forEach(row=>tbody.appendChild(row));
|
||||
}
|
||||
|
||||
async function loadArpTable(){
|
||||
const tableBody=document.getElementById("arpTableBody");
|
||||
|
||||
try{
|
||||
const response=await fetch("/arp/json");
|
||||
const data=await response.json();
|
||||
|
||||
tableBody.innerHTML="";
|
||||
|
||||
if(!data.arp_table||data.arp_table.length===0){
|
||||
tableBody.innerHTML="<tr><td colspan='4' class='text-center'>Geen data beschikbaar.</td></tr>";
|
||||
return;
|
||||
}
|
||||
|
||||
data.arp_table.forEach((entry,index)=>{
|
||||
const row=document.createElement("tr");
|
||||
row.setAttribute("data-index",index);
|
||||
row.innerHTML=`
|
||||
<td>${entry.address}</td>
|
||||
<td><a href="/port/${entry.interface}" target="_blank">${entry.interface}</a></td>
|
||||
<td><a href="https://bgp.tools/search?q=${entry.link_layer_address}" target="_blank">${entry.link_layer_address}</a></td>
|
||||
<td>${entry.state}</td>
|
||||
`;
|
||||
tableBody.appendChild(row);
|
||||
});
|
||||
}catch(err){
|
||||
tableBody.innerHTML="<tr><td colspan='4' class='text-center text-danger'>Fout bij ophalen van data.</td></tr>";
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("DOMContentLoaded",()=>{
|
||||
loadArpTable();
|
||||
});
|
||||
|
||||
function refreshArpTable(){
|
||||
const searchInput=document.getElementById("arpSearch");
|
||||
const refreshIcon=document.getElementById("refreshIcon");
|
||||
const refreshSpinner=document.getElementById("refreshSpinner");
|
||||
|
||||
searchInput.value="";
|
||||
|
||||
refreshIcon.classList.add("d-none");
|
||||
refreshSpinner.classList.remove("d-none");
|
||||
|
||||
loadArpTable().then(()=>{
|
||||
refreshIcon.classList.remove("d-none");
|
||||
refreshSpinner.classList.add("d-none");
|
||||
});
|
||||
}
|
||||
95
static/js/pages/bgp-route.js
Normal file
95
static/js/pages/bgp-route.js
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
function detectIpVersion(ip){
|
||||
const ipv4Regex=/^(\d{1,3}\.){3}\d{1,3}(\/\d{1,2})?$/;
|
||||
const ipv6Regex=/^([0-9a-fA-F:]+)(\/\d{1,3})?$/;
|
||||
|
||||
if(ipv4Regex.test(ip)){
|
||||
return"ipv4";
|
||||
}else if(ipv6Regex.test(ip)){
|
||||
return"ipv6";
|
||||
}else{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function highlightBGPOutput(text){
|
||||
text=text.replace(/\b215085\b/g,'<span class="asn215085-highlight">215085</span>');
|
||||
|
||||
text=text.replace(/\b\d{4,6}\b/g,match=>{
|
||||
if(match==='215085')return match;
|
||||
return`<span class="asn-highlight">${match}</span>`;
|
||||
});
|
||||
|
||||
text=text.split('\n').map(line=>{
|
||||
if(line.toLowerCase().includes('best')){
|
||||
return`<strong class="best-line">${line}</strong>`;
|
||||
}
|
||||
return line;
|
||||
}).join('\n');
|
||||
|
||||
text=text.split('\n').map(line=>{
|
||||
if(line.toLowerCase().includes('table entry')){
|
||||
return`<strong class="best-line">${line}</strong>`;
|
||||
}
|
||||
return line;
|
||||
}).join('\n');
|
||||
|
||||
text=text.split('\n').map(line=>{
|
||||
if(line.toLowerCase().includes('multipath')){
|
||||
return`<strong class="best-line">${line}</strong>`;
|
||||
}
|
||||
return line;
|
||||
}).join('\n');
|
||||
|
||||
return text.replace(/\n/g,'<br>');
|
||||
}
|
||||
|
||||
function displayBGPRoute(data){
|
||||
const outputElem=document.getElementById("bgpOutput");
|
||||
const rawText=data.data||JSON.stringify(data,null,2);
|
||||
const highlighted=highlightBGPOutput(rawText);
|
||||
outputElem.innerHTML=highlighted;
|
||||
}
|
||||
|
||||
async function loadBGPRoute(){
|
||||
const outputElem=document.getElementById("bgpOutput");
|
||||
const prefix=document.getElementById("prefixInput").value.trim();
|
||||
|
||||
if(!prefix){
|
||||
outputElem.textContent="Enter a valid prefix first.";
|
||||
return;
|
||||
}
|
||||
|
||||
const ip_version=detectIpVersion(prefix);
|
||||
if(!ip_version){
|
||||
outputElem.textContent="Invalid IP address or prefix.";
|
||||
return;
|
||||
}
|
||||
|
||||
outputElem.textContent="Loading...";
|
||||
|
||||
const postData={
|
||||
ip_version:ip_version,
|
||||
bgprouteprefix:prefix
|
||||
};
|
||||
|
||||
try{
|
||||
const response=await fetch("/bgp-route/lookup",{
|
||||
method:"POST",
|
||||
headers:{
|
||||
"Content-Type":"application/json",
|
||||
},
|
||||
body:JSON.stringify(postData),
|
||||
});
|
||||
if(!response.ok)throw new Error("Network response was not ok");
|
||||
const data=await response.json();
|
||||
|
||||
if(data.error){
|
||||
outputElem.textContent="Error: "+data.error;
|
||||
return;
|
||||
}
|
||||
|
||||
displayBGPRoute(data);
|
||||
}catch(error){
|
||||
outputElem.textContent="Error retrieving data: "+error.message;
|
||||
}
|
||||
}
|
||||
169
static/js/pages/bgp.js
Normal file
169
static/js/pages/bgp.js
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
function filterTable(searchInputId,tableId){
|
||||
const input=document.getElementById(searchInputId);
|
||||
const filter=input.value.toUpperCase();
|
||||
const table=document.getElementById(tableId);
|
||||
const tr=table.getElementsByTagName("tr");
|
||||
|
||||
for(let i=1;i<tr.length;i++){
|
||||
tr[i].style.display="none";
|
||||
const td=tr[i].getElementsByTagName("td");
|
||||
for(let j=0;j<td.length;j++){
|
||||
if(td[j]){
|
||||
const txtValue=td[j].textContent||td[j].innerText;
|
||||
if(txtValue.toUpperCase().indexOf(filter)>-1){
|
||||
tr[i].style.display="";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sortTable(tableId,columnIndex,th){
|
||||
const table=document.getElementById(tableId);
|
||||
const tbody=table.tBodies[0];
|
||||
const rows=Array.from(tbody.rows);
|
||||
const headers=Array.from(th.parentNode.children);
|
||||
|
||||
headers.forEach(header=>{
|
||||
if(header!==th){
|
||||
header.classList.remove('asc','desc');
|
||||
header.setAttribute('data-sort-state','none');
|
||||
}
|
||||
});
|
||||
|
||||
let currentState=th.getAttribute('data-sort-state')||'none';
|
||||
let newState=currentState==='none'?'asc':currentState==='asc'?'desc':'none';
|
||||
|
||||
th.classList.remove('asc','desc');
|
||||
if(newState!=='none')th.classList.add(newState);
|
||||
th.setAttribute('data-sort-state',newState);
|
||||
|
||||
if(newState==='none'){
|
||||
rows.sort((a,b)=>parseInt(a.getAttribute('data-index'))-parseInt(b.getAttribute('data-index')));
|
||||
}else{
|
||||
rows.sort((a,b)=>{
|
||||
const aText=a.cells[columnIndex].textContent.trim();
|
||||
const bText=b.cells[columnIndex].textContent.trim();
|
||||
|
||||
const aNum=parseFloat(aText.replace(/[^0-9.-]/g,''));
|
||||
const bNum=parseFloat(bText.replace(/[^0-9.-]/g,''));
|
||||
const isNumeric=!isNaN(aNum)&&!isNaN(bNum);
|
||||
|
||||
return newState==='asc'
|
||||
?(isNumeric?aNum-bNum:aText.localeCompare(bText))
|
||||
:(isNumeric?bNum-aNum:bText.localeCompare(aText));
|
||||
});
|
||||
}
|
||||
|
||||
rows.forEach(row=>tbody.appendChild(row));
|
||||
}
|
||||
|
||||
async function loadBgpTables(){
|
||||
const ipv4SummaryEl=document.getElementById("ipv4Summary");
|
||||
const ipv6SummaryEl=document.getElementById("ipv6Summary");
|
||||
const ipv4Body=document.getElementById("ipv4TableBody");
|
||||
const ipv6Body=document.getElementById("ipv6TableBody");
|
||||
|
||||
try{
|
||||
const response=await fetch("/bgp/json");
|
||||
const data=await response.json();
|
||||
|
||||
ipv4SummaryEl.innerHTML=`
|
||||
<p>BGP Router ID: ${data.ipv4_info.router_id}, Local AS Number: <a href="https://bgp.tools/search?q=${data.ipv4_info.local_as}" target="_blank" rel="noopener noreferrer">${data.ipv4_info.local_as}</a>, VRF ID: ${data.ipv4_info.vrf_id}</p>
|
||||
<p>BGP Table Version:${data.ipv4_info.table_version}</p>
|
||||
<p>RIB Entries:${data.ipv4_info.rib_entries},using ${data.ipv4_info.rib_memory}</p>
|
||||
<p>Peers:${data.ipv4_info.peers},using ${data.ipv4_info.peers_memory}</p>
|
||||
`;
|
||||
|
||||
ipv6SummaryEl.innerHTML=`
|
||||
<p>BGP Router ID: ${data.ipv6_info.router_id}, Local AS Number: <a href="https://bgp.tools/search?q=${data.ipv6_info.local_as}" target="_blank" rel="noopener noreferrer">${data.ipv6_info.local_as}</a>, VRF ID: ${data.ipv6_info.vrf_id}</p>
|
||||
<p>BGP Table Version:${data.ipv6_info.table_version}</p>
|
||||
<p>RIB Entries:${data.ipv6_info.rib_entries},using ${data.ipv6_info.rib_memory}</p>
|
||||
<p>Peers:${data.ipv6_info.peers},using ${data.ipv6_info.peers_memory}</p>
|
||||
`;
|
||||
|
||||
ipv4Body.innerHTML="";
|
||||
if(data.ipv4_peers.length===0){
|
||||
ipv4Body.innerHTML=`<tr><td colspan="12" class="text-center">No data available.</td></tr>`;
|
||||
}else{
|
||||
data.ipv4_peers.forEach((peer,index)=>{
|
||||
const row=document.createElement("tr");
|
||||
row.setAttribute("data-index",index);
|
||||
row.innerHTML=`
|
||||
<td>${peer.neighbor}</td>
|
||||
<td>${peer.version}</td>
|
||||
<td><a href="https://bgp.tools/search?q=${peer.as_number}" target="_blank">${peer.as_number}</a></td>
|
||||
<td>${peer.msg_received}</td>
|
||||
<td>${peer.msg_sent}</td>
|
||||
<td>${peer.table_version}</td>
|
||||
<td>${peer.in_queue}</td>
|
||||
<td>${peer.out_queue}</td>
|
||||
<td>${peer.up_down}</td>
|
||||
<td>${peer.state_pfx_rcd}</td>
|
||||
<td>${peer.prefix_sent}</td>
|
||||
<td>${peer.description}</td>
|
||||
`;
|
||||
ipv4Body.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
ipv6Body.innerHTML="";
|
||||
if(data.ipv6_peers.length===0){
|
||||
ipv6Body.innerHTML=`<tr><td colspan="12" class="text-center">No data available.</td></tr>`;
|
||||
}else{
|
||||
data.ipv6_peers.forEach((peer,index)=>{
|
||||
const row=document.createElement("tr");
|
||||
row.setAttribute("data-index",index);
|
||||
row.innerHTML=`
|
||||
<td>${peer.neighbor}</td>
|
||||
<td>${peer.version}</td>
|
||||
<td><a href="https://bgp.tools/search?q=${peer.as_number}" target="_blank">${peer.as_number}</a></td>
|
||||
<td>${peer.msg_received}</td>
|
||||
<td>${peer.msg_sent}</td>
|
||||
<td>${peer.table_version}</td>
|
||||
<td>${peer.in_queue}</td>
|
||||
<td>${peer.out_queue}</td>
|
||||
<td>${peer.up_down}</td>
|
||||
<td>${peer.state_pfx_rcd}</td>
|
||||
<td>${peer.prefix_sent}</td>
|
||||
<td>${peer.description}</td>
|
||||
`;
|
||||
ipv6Body.appendChild(row);
|
||||
});
|
||||
}
|
||||
}catch(error){
|
||||
ipv4SummaryEl.textContent="Error fetching IPv4 data.";
|
||||
ipv6SummaryEl.textContent="Error fetching IPv6 data.";
|
||||
ipv4Body.innerHTML=`<tr><td colspan="12" class="text-center text-danger">Error retrieving data.</td></tr>`;
|
||||
ipv6Body.innerHTML=`<tr><td colspan="12" class="text-center text-danger">Error retrieving data.</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("DOMContentLoaded",()=>{
|
||||
loadBgpTables();
|
||||
});
|
||||
|
||||
function refreshBGPTable(){
|
||||
const searchInput=document.getElementById("ipv4Search");
|
||||
const searchInput2=document.getElementById("ipv6Search");
|
||||
const refreshIcon=document.getElementById("refreshIcon");
|
||||
const refreshIcon2=document.getElementById("refreshIcon2");
|
||||
const refreshSpinner=document.getElementById("refreshSpinner");
|
||||
const refreshSpinner2=document.getElementById("refreshSpinner2");
|
||||
|
||||
searchInput.value="";
|
||||
searchInput2.value="";
|
||||
|
||||
refreshIcon.classList.add("d-none");
|
||||
refreshIcon2.classList.add("d-none");
|
||||
refreshSpinner.classList.remove("d-none");
|
||||
refreshSpinner2.classList.remove("d-none");
|
||||
|
||||
loadBgpTables().then(()=>{
|
||||
refreshIcon.classList.remove("d-none");
|
||||
refreshIcon2.classList.remove("d-none");
|
||||
refreshSpinner.classList.add("d-none");
|
||||
refreshSpinner2.classList.add("d-none");
|
||||
});
|
||||
}
|
||||
113
static/js/pages/interfaces.js
Normal file
113
static/js/pages/interfaces.js
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
function filterTable(searchInputId,tableId){
|
||||
const input=document.getElementById(searchInputId);
|
||||
const filter=input.value.toUpperCase();
|
||||
const table=document.getElementById(tableId);
|
||||
const tr=table.getElementsByTagName("tr");
|
||||
|
||||
for(let i=1;i<tr.length;i++){
|
||||
tr[i].style.display="none";
|
||||
const td=tr[i].getElementsByTagName("td");
|
||||
for(let j=0;j<td.length;j++){
|
||||
if(td[j]){
|
||||
const txtValue=td[j].textContent||td[j].innerText;
|
||||
if(txtValue.toUpperCase().indexOf(filter)>-1){
|
||||
tr[i].style.display="";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sortTable(tableId,columnIndex,th){
|
||||
const table=document.getElementById(tableId);
|
||||
const tbody=table.tBodies[0];
|
||||
const rows=Array.from(tbody.rows);
|
||||
const headers=Array.from(th.parentNode.children);
|
||||
|
||||
headers.forEach(header=>{
|
||||
if(header!==th){
|
||||
header.classList.remove('asc','desc');
|
||||
header.setAttribute('data-sort-state','none');
|
||||
}
|
||||
});
|
||||
|
||||
let currentState=th.getAttribute('data-sort-state')||'none';
|
||||
let newState=currentState==='none'?'asc':currentState==='asc'?'desc':'none';
|
||||
|
||||
th.classList.remove('asc','desc');
|
||||
if(newState!=='none')th.classList.add(newState);
|
||||
th.setAttribute('data-sort-state',newState);
|
||||
|
||||
if(newState==='none'){
|
||||
rows.sort((a,b)=>parseInt(a.getAttribute('data-index'))-parseInt(b.getAttribute('data-index')));
|
||||
}else{
|
||||
rows.sort((a,b)=>{
|
||||
const aText=a.cells[columnIndex].textContent.trim();
|
||||
const bText=b.cells[columnIndex].textContent.trim();
|
||||
const aNum=parseFloat(aText.replace(/[^0-9.-]/g,''));
|
||||
const bNum=parseFloat(bText.replace(/[^0-9.-]/g,''));
|
||||
const isNumeric=!isNaN(aNum)&&!isNaN(bNum);
|
||||
|
||||
return newState==='asc'
|
||||
?(isNumeric?aNum-bNum:aText.localeCompare(bText))
|
||||
:(isNumeric?bNum-aNum:bText.localeCompare(aText));
|
||||
});
|
||||
}
|
||||
|
||||
rows.forEach(row=>tbody.appendChild(row));
|
||||
}
|
||||
|
||||
async function loadInterfaceTable(){
|
||||
const tableBody=document.getElementById("interfaceTableBody");
|
||||
const loadingRow=document.getElementById("interface-loading-row");
|
||||
|
||||
try{
|
||||
const response=await fetch("/interfaces/json");
|
||||
const data=await response.json();
|
||||
|
||||
tableBody.innerHTML="";
|
||||
|
||||
if(!data.interface_table||data.interface_table.length===0){
|
||||
tableBody.innerHTML="<tr><td colspan='7' class='text-center'>No data available.</td></tr>";
|
||||
return;
|
||||
}
|
||||
|
||||
data.interface_table.forEach((entry,index)=>{
|
||||
const row=document.createElement("tr");
|
||||
row.setAttribute("data-index",index);
|
||||
row.innerHTML=`
|
||||
<td><a href="/port/${entry.interface}" target="_blank">${entry.interface}</a></td>
|
||||
<td>${entry.ip_address}</td>
|
||||
<td><a href="https://bgp.tools/search?q=${entry.mac_address}" target="_blank">${entry.mac_address}</a></td>
|
||||
<td>${entry.vrf}</td>
|
||||
<td>${entry.mtu}</td>
|
||||
<td>${entry.status}</td>
|
||||
<td>${entry.description}</td>
|
||||
`;
|
||||
tableBody.appendChild(row);
|
||||
});
|
||||
}catch(err){
|
||||
tableBody.innerHTML="<tr><td colspan='7' class='text-center text-danger'>Error retrieving data.</td></tr>";
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("DOMContentLoaded",()=>{
|
||||
loadInterfaceTable();
|
||||
});
|
||||
|
||||
function refreshTable(){
|
||||
const searchInput=document.getElementById("interfaceSearch");
|
||||
const refreshIcon=document.getElementById("refreshIcon");
|
||||
const refreshSpinner=document.getElementById("refreshSpinner");
|
||||
|
||||
searchInput.value="";
|
||||
|
||||
refreshIcon.classList.add("d-none");
|
||||
refreshSpinner.classList.remove("d-none");
|
||||
|
||||
loadInterfaceTable().then(()=>{
|
||||
refreshIcon.classList.remove("d-none");
|
||||
refreshSpinner.classList.add("d-none");
|
||||
});
|
||||
}
|
||||
108
static/js/pages/neighbors.js
Normal file
108
static/js/pages/neighbors.js
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
function filterTable(searchInputId,tableId){
|
||||
const input=document.getElementById(searchInputId);
|
||||
const filter=input.value.toUpperCase();
|
||||
const table=document.getElementById(tableId);
|
||||
const tr=table.getElementsByTagName("tr");
|
||||
|
||||
for(let i=1;i<tr.length;i++){
|
||||
tr[i].style.display="none";
|
||||
const td=tr[i].getElementsByTagName("td");
|
||||
for(let j=0;j<td.length;j++){
|
||||
if(td[j]){
|
||||
const txtValue=td[j].textContent||td[j].innerText;
|
||||
if(txtValue.toUpperCase().indexOf(filter)>-1){
|
||||
tr[i].style.display="";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sortTable(tableId,columnIndex,th){
|
||||
const table=document.getElementById(tableId);
|
||||
const tbody=table.tBodies[0];
|
||||
const rows=Array.from(tbody.rows);
|
||||
const headers=Array.from(th.parentNode.children);
|
||||
|
||||
headers.forEach(header=>{
|
||||
if(header!==th){
|
||||
header.classList.remove('asc','desc');
|
||||
header.setAttribute('data-sort-state','none');
|
||||
}
|
||||
});
|
||||
|
||||
let currentState=th.getAttribute('data-sort-state')||'none';
|
||||
let newState=currentState==='none'?'asc':currentState==='asc'?'desc':'none';
|
||||
|
||||
th.classList.remove('asc','desc');
|
||||
if(newState!=='none')th.classList.add(newState);
|
||||
th.setAttribute('data-sort-state',newState);
|
||||
|
||||
if(newState==='none'){
|
||||
rows.sort((a,b)=>parseInt(a.getAttribute('data-index'))-parseInt(b.getAttribute('data-index')));
|
||||
}else{
|
||||
rows.sort((a,b)=>{
|
||||
const aText=a.cells[columnIndex].textContent.trim();
|
||||
const bText=b.cells[columnIndex].textContent.trim();
|
||||
const aNum=parseFloat(aText.replace(/[^0-9.-]/g,''));
|
||||
const bNum=parseFloat(bText.replace(/[^0-9.-]/g,''));
|
||||
const isNumeric=!isNaN(aNum)&&!isNaN(bNum);
|
||||
return newState==='asc'
|
||||
?(isNumeric?aNum-bNum:aText.localeCompare(bText))
|
||||
:(isNumeric?bNum-aNum:bText.localeCompare(aText));
|
||||
});
|
||||
}
|
||||
|
||||
rows.forEach(row=>tbody.appendChild(row));
|
||||
}
|
||||
|
||||
async function loadneighborsTable(){
|
||||
const tableBody=document.getElementById("neighborsTableBody");
|
||||
|
||||
try{
|
||||
const response=await fetch("/neighbors/json");
|
||||
const data=await response.json();
|
||||
|
||||
tableBody.innerHTML="";
|
||||
|
||||
if(!data.neighbors_table||data.neighbors_table.length===0){
|
||||
tableBody.innerHTML="<tr><td colspan='4' class='text-center'>No data available.</td></tr>";
|
||||
return;
|
||||
}
|
||||
|
||||
data.neighbors_table.forEach((entry,index)=>{
|
||||
const row=document.createElement("tr");
|
||||
row.setAttribute("data-index",index);
|
||||
row.innerHTML=`
|
||||
<td>${entry.address}</td>
|
||||
<td><a href="/port/${entry.interface}" target="_blank">${entry.interface}</a></td>
|
||||
<td><a href="https://bgp.tools/search?q=${entry.link_layer_address}" target="_blank">${entry.link_layer_address}</a></td>
|
||||
<td>${entry.state}</td>
|
||||
`;
|
||||
tableBody.appendChild(row);
|
||||
});
|
||||
}catch(err){
|
||||
tableBody.innerHTML="<tr><td colspan='4' class='text-center text-danger'>Error retrieving data.</td></tr>";
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("DOMContentLoaded",()=>{
|
||||
loadneighborsTable();
|
||||
});
|
||||
|
||||
function refreshNeighborsTable(){
|
||||
const searchInput=document.getElementById("neighborsSearch");
|
||||
const refreshIcon=document.getElementById("refreshIcon");
|
||||
const refreshSpinner=document.getElementById("refreshSpinner");
|
||||
|
||||
searchInput.value="";
|
||||
|
||||
refreshIcon.classList.add("d-none");
|
||||
refreshSpinner.classList.remove("d-none");
|
||||
|
||||
loadneighborsTable().then(()=>{
|
||||
refreshIcon.classList.remove("d-none");
|
||||
refreshSpinner.classList.add("d-none");
|
||||
});
|
||||
}
|
||||
17
templates/404.html
Normal file
17
templates/404.html
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}AS215085 - Router tools | 404{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section id="overview">
|
||||
<h2>404 Not found</h2>
|
||||
<center><img src="../static/img/404.jpg" alt="404 GIF" width="60%">
|
||||
<h3>Ooops! sorry this page was not found</h3>
|
||||
<br>
|
||||
<a href="/" style="text-decoration: none;">
|
||||
<button style="background-color: #07AAF9; color: white; border: none; border-radius: 25px; padding: 15px 30px; font-size: 18px;">
|
||||
Go Home
|
||||
</button>
|
||||
</a></center>
|
||||
</section>
|
||||
{% endblock %}
|
||||
38
templates/arp.html
Normal file
38
templates/arp.html
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}AS215085 - Router tools | ARP table{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<script src="../static/js/pages/arp.js"></script>
|
||||
<section id="arp">
|
||||
<h2>ARP table</h2>
|
||||
<div class="input-group mb-3">
|
||||
<input type="text" id="arpSearch" class="form-control" placeholder="Search in ARP table..." onkeyup="filterTable('arpSearch', 'arpTable')">
|
||||
<button id="refreshButton" class="btn btn-outline-primary" type="button" onclick="refreshArpTable()">
|
||||
<span id="refreshIcon">⟳</span>
|
||||
<span id="refreshSpinner" class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
|
||||
Refresh data
|
||||
</button>
|
||||
</div>
|
||||
<table class="striped table table-bordered" id="arpTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="sortable" onclick="sortTable('arpTable', 0, this)">Address <span class="sort-arrow"></span></th>
|
||||
<th class="sortable" onclick="sortTable('arpTable', 1, this)">Interface <span class="sort-arrow"></span></th>
|
||||
<th class="sortable" onclick="sortTable('arpTable', 2, this)">Link Layer Address <span class="sort-arrow"></span></th>
|
||||
<th class="sortable" onclick="sortTable('arpTable', 3, this)">State <span class="sort-arrow"></span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="arpTableBody">
|
||||
<tr id="arp-loading-row">
|
||||
<td colspan="4" class="text-center">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p><a href="/arp/json">JSON version</a></p>
|
||||
</section>
|
||||
{% endblock %}
|
||||
48
templates/base.html
Normal file
48
templates/base.html
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}AS215085 - Network tools{% endblock %}</title>
|
||||
<link rel="icon" type="image/png" href="../static/img/favicon.png">
|
||||
<script src="../static/js/bootstrap.bundle.min.js"></script>
|
||||
<link href="../static/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="../static/css/style.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="header-content">
|
||||
<a href="/"> <img src="../static/img/as215085-logo.png" alt="AS215085 Logo" style="height: 120px; opacity: 1;!important"> </a>
|
||||
<h2><b>{{ hostname }}</b></h2>
|
||||
<p><b>Proudly delivering the backbone for PixelHosting’s services</b></p>
|
||||
</div>
|
||||
</header>
|
||||
<nav class="navbar navbar-expand-lg navbar-light" style="background-color: #07AAF9;">
|
||||
<div class="container-fluid">
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav justify-content-center w-100">
|
||||
<li class="nav-item"><a class="nav-link text-white" href="/">Home</a></li>
|
||||
<li class="nav-item"><a class="nav-link text-white" href="/bgp">BGP summary</a></li>
|
||||
<li class="nav-item"><a class="nav-link text-white" href="/bgp-route">BGP route</a></li>
|
||||
<li class="nav-item"><a class="nav-link text-white" href="/arp">ARP table</a></li>
|
||||
<li class="nav-item"><a class="nav-link text-white" href="/neighbors">Neighbor table</a></li>
|
||||
<li class="nav-item"><a class="nav-link text-white" href="/interfaces">Interfaces table</a></li>
|
||||
<li class="nav-item"><a class="nav-link text-white" href="/stats">Stats</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<main>
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</main>
|
||||
<footer>
|
||||
<p>© 2020 – <span id="year"></span> AS215085 (PixelHosting). All rights reserved.</p>
|
||||
<script> document.getElementById("year").textContent = new Date().getFullYear(); </script>
|
||||
</footer>
|
||||
<script src="../static/js/materialize.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
22
templates/bgp-route.html
Normal file
22
templates/bgp-route.html
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}AS215085 - Router tools | BGP route lookup{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<script src="../static/js/pages/bgp-route.js"></script>
|
||||
<section id="bgp-route-lookup">
|
||||
<h2>BGP route lookup</h2>
|
||||
<div class="input-group mb-3">
|
||||
<input
|
||||
type="text"
|
||||
id="prefixInput"
|
||||
class="form-control"
|
||||
placeholder="Type the prefix here, for example, 2606:4700:4700::/48 or 1.1.1.0/24"
|
||||
aria-label="Enter a prefix"
|
||||
onkeydown="if(event.key === 'Enter') loadBGPRoute()"
|
||||
/>
|
||||
<button class="btn btn-outline-primary" type="button" onclick="loadBGPRoute()">Lookup</button>
|
||||
</div>
|
||||
<pre id="bgpOutput" aria-label="BGP Route Lookup output">Enter a prefix and click Lookup.</pre>
|
||||
</section>
|
||||
{% endblock %}
|
||||
88
templates/bgp.html
Normal file
88
templates/bgp.html
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}AS215085 - Router tools | BGP table{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<script src="../static/js/pages/bgp.js"></script>
|
||||
<section id="bgp-v4">
|
||||
<h2>IPv4 Unicast Summary</h2>
|
||||
<p id="ipv4Summary">Loading summary...</p>
|
||||
<div class="input-group mb-3">
|
||||
<input type="text" id="ipv4Search" class="form-control" placeholder="Search in IPv4 table..." onkeyup="filterTable('ipv4Search', 'ipv4Table')">
|
||||
<button id="refreshButton" class="btn btn-outline-primary" type="button" onclick="refreshBGPTable()">
|
||||
<span id="refreshIcon">⟳</span>
|
||||
<span id="refreshSpinner" class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
|
||||
Refresh data
|
||||
</button>
|
||||
</div>
|
||||
<table class="striped" id="ipv4Table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="sortable" onclick="sortTable('ipv4Table', 0, this)">Neighbor <span class="sort-arrow"></span></th>
|
||||
<th class="sortable" onclick="sortTable('ipv4Table', 1, this)">Version <span class="sort-arrow"></span></th>
|
||||
<th class="sortable" onclick="sortTable('ipv4Table', 2, this)">AS Number <span class="sort-arrow"></span></th>
|
||||
<th class="sortable" onclick="sortTable('ipv4Table', 3, this)">Messages Received <span class="sort-arrow"></span></th>
|
||||
<th class="sortable" onclick="sortTable('ipv4Table', 4, this)">Messages Sent <span class="sort-arrow"></span></th>
|
||||
<th class="sortable" onclick="sortTable('ipv4Table', 5, this)">Table Version <span class="sort-arrow"></span></th>
|
||||
<th class="sortable" onclick="sortTable('ipv4Table', 6, this)">Inbound Queue <span class="sort-arrow"></span></th>
|
||||
<th class="sortable" onclick="sortTable('ipv4Table', 7, this)">Outbound Queue <span class="sort-arrow"></span></th>
|
||||
<th class="sortable" onclick="sortTable('ipv4Table', 8, this)">Up/Down <span class="sort-arrow"></span></th>
|
||||
<th class="sortable" onclick="sortTable('ipv4Table', 9, this)">State/PfxRcd <span class="sort-arrow"></span></th>
|
||||
<th class="sortable" onclick="sortTable('ipv4Table', 10, this)">Prefix Sent <span class="sort-arrow"></span></th>
|
||||
<th class="sortable" onclick="sortTable('ipv4Table', 11, this)">Description <span class="sort-arrow"></span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="ipv4TableBody">
|
||||
<tr>
|
||||
<td colspan="12" class="text-center">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p><a href="/bgp/json">JSON version</a></p>
|
||||
</section>
|
||||
|
||||
<section id="bgp-v6">
|
||||
<h2>IPv6 Unicast Summary</h2>
|
||||
<p id="ipv6Summary">Loading summary...</p>
|
||||
<div class="input-group mb-3">
|
||||
<input type="text" id="ipv6Search" class="form-control" placeholder="Search in IPv6 table..." onkeyup="filterTable('ipv6Search', 'ipv6Table')">
|
||||
<button id="refreshButton" class="btn btn-outline-primary" type="button" onclick="refreshBGPTable()">
|
||||
<span id="refreshIcon2">⟳</span>
|
||||
<span id="refreshSpinner2" class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
|
||||
Refresh data
|
||||
</button>
|
||||
</div>
|
||||
<table class="striped" id="ipv6Table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="sortable" onclick="sortTable('ipv6Table', 0, this)">Neighbor <span class="sort-arrow"></span></th>
|
||||
<th class="sortable" onclick="sortTable('ipv6Table', 1, this)">Version <span class="sort-arrow"></span></th>
|
||||
<th class="sortable" onclick="sortTable('ipv6Table', 2, this)">AS Number <span class="sort-arrow"></span></th>
|
||||
<th class="sortable" onclick="sortTable('ipv6Table', 3, this)">Messages Received <span class="sort-arrow"></span></th>
|
||||
<th class="sortable" onclick="sortTable('ipv6Table', 4, this)">Messages Sent <span class="sort-arrow"></span></th>
|
||||
<th class="sortable" onclick="sortTable('ipv6Table', 5, this)">Table Version <span class="sort-arrow"></span></th>
|
||||
<th class="sortable" onclick="sortTable('ipv6Table', 6, this)">Inbound Queue <span class="sort-arrow"></span></th>
|
||||
<th class="sortable" onclick="sortTable('ipv6Table', 7, this)">Outbound Queue <span class="sort-arrow"></span></th>
|
||||
<th class="sortable" onclick="sortTable('ipv6Table', 8, this)">Up/Down <span class="sort-arrow"></span></th>
|
||||
<th class="sortable" onclick="sortTable('ipv6Table', 9, this)">State/PfxRcd <span class="sort-arrow"></span></th>
|
||||
<th class="sortable" onclick="sortTable('ipv6Table', 10, this)">Prefix Sent <span class="sort-arrow"></span></th>
|
||||
<th class="sortable" onclick="sortTable('ipv6Table', 11, this)">Description <span class="sort-arrow"></span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="ipv6TableBody">
|
||||
<tr>
|
||||
<td colspan="12" class="text-center">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p><a href="/bgp/json">JSON version</a></p>
|
||||
</section>
|
||||
{% endblock %}
|
||||
25
templates/index.html
Normal file
25
templates/index.html
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}AS215085 - Router tools | Home{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section id="overview">
|
||||
<h2>Welcome</h2>
|
||||
<p>Welcome to the <b>AS215085 Network tools</b></p>
|
||||
<p>On this tool you cn get in depth insight into our network</p>
|
||||
<p>In case of emergency you can contact us at: noc AT pixelhosting DOT nl</p>
|
||||
<p>PeeringDB: <a href="https://www.peeringdb.com/net/35968" target="_blank" style="color: #07AAF9;">https://www.peeringdb.com/net/35968</a></p>
|
||||
<p>BGPTools: <a href="https://bgp.tools/as/215085" target="_blank" style="color: #07AAF9;">https://bgp.tools/as/215085</a></p>
|
||||
<p>Lookingglas: <a href="https://lookingglass.as215085.net" target="_blank" style="color: #07AAF9;">https://lookingglass.as215085.net</a></p>
|
||||
<p>Geofeed: <a href="https://as215085.net/geofeed.csv" target="_blank" style="color: #07AAF9;">https://as215085.net/geofeed.csv</a></p>
|
||||
</section>
|
||||
<section id="status">
|
||||
<h2>Status</h2>
|
||||
<p>Here you can find the network status of <b>AS215085</b>: <a href="https://status.as215085.net" target="_blank" style="color: #07AAF9;">https://status.as215085.net</a></p>
|
||||
<p>Here you can find the service status of <b>PixelHosting</b>: <a href="https://status.pixelhosting.nl/" target="_blank" style="color: #07AAF9;">https://status.pixelhosting.nl</a></p>
|
||||
</section>
|
||||
<section id="bgptools-map">
|
||||
<h2>BGP.Tools network map</h2>
|
||||
<img id="pathimg" usemap="#world" src="{{ bgp_image }}">
|
||||
</section>
|
||||
{% endblock %}
|
||||
42
templates/interfaces.html
Normal file
42
templates/interfaces.html
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}AS215085 - Router tools | Interfaces{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<script src="../static/js/pages/interfaces.js"></script>
|
||||
<section id="interfaces">
|
||||
<h2>Interfaces</h2>
|
||||
<div class="input-group mb-3">
|
||||
<input type="text" id="interfaceSearch" class="form-control" placeholder="Search in Interface table..." onkeyup="filterTable('interfaceSearch', 'interfaceTable')">
|
||||
<button id="refreshButton" class="btn btn-outline-primary" type="button" onclick="refreshTable()">
|
||||
<span id="refreshIcon">⟳</span>
|
||||
<span id="refreshSpinner" class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
|
||||
Refresh data
|
||||
</button>
|
||||
</div>
|
||||
<p></p> <p>Codes: S - State, L - Link, u - Up, D - Down, A - Admin Down</p>
|
||||
<table class="striped table table-bordered" id="interfaceTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="sortable" onclick="sortTable('interfaceTable', 0, this)">Interface <span class="sort-arrow"></span></th>
|
||||
<th class="sortable" onclick="sortTable('interfaceTable', 1, this)">IP Address <span class="sort-arrow"></span></th>
|
||||
<th class="sortable" onclick="sortTable('interfaceTable', 2, this)">MAC Address <span class="sort-arrow"></span></th>
|
||||
<th class="sortable" onclick="sortTable('interfaceTable', 3, this)">VRF <span class="sort-arrow"></span></th>
|
||||
<th class="sortable" onclick="sortTable('interfaceTable', 4, this)">MTU <span class="sort-arrow"></span></th>
|
||||
<th class="sortable" onclick="sortTable('interfaceTable', 5, this)">Status <span class="sort-arrow"></span></th>
|
||||
<th class="sortable" onclick="sortTable('interfaceTable', 6, this)">Description <span class="sort-arrow"></span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="interfaceTableBody">
|
||||
<tr id="interface-loading-row">
|
||||
<td colspan="7" class="text-center">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p><a href="/interfaces/json">JSON version</a></p>
|
||||
</section>
|
||||
{% endblock %}
|
||||
38
templates/neighbors.html
Normal file
38
templates/neighbors.html
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}AS215085 - Router tools | Neighbor table{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<script src="../static/js/pages/neighbors.js"></script>
|
||||
<section id="neighbor">
|
||||
<h2>Neighbor table</h2>
|
||||
<div class="input-group mb-3">
|
||||
<input type="text" id="neighborsSearch" class="form-control" placeholder="Search in Neighbors table..." onkeyup="filterTable('neighborsSearch', 'neighborsTable')">
|
||||
<button id="refreshButton" class="btn btn-outline-primary" type="button" onclick="refreshNeighborsTable()">
|
||||
<span id="refreshIcon">⟳</span>
|
||||
<span id="refreshSpinner" class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
|
||||
Refresh data
|
||||
</button>
|
||||
</div>
|
||||
<table class="striped table table-bordered" id="neighborsTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="sortable" onclick="sortTable('neighborsTable', 0, this)">Address <span class="sort-arrow"></span></th>
|
||||
<th class="sortable" onclick="sortTable('neighborsTable', 1, this)">Interface <span class="sort-arrow"></span></th>
|
||||
<th class="sortable" onclick="sortTable('neighborsTable', 2, this)">Link Layer Address <span class="sort-arrow"></span></th>
|
||||
<th class="sortable" onclick="sortTable('neighborsTable', 3, this)">State <span class="sort-arrow"></span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="neighborsTableBody">
|
||||
<tr id="arp-loading-row">
|
||||
<td colspan="4" class="text-center">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p><a href="/neighbors/json">JSON version</a></p>
|
||||
</section>
|
||||
{% endblock %}
|
||||
25
templates/port.html
Normal file
25
templates/port.html
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}AS215085 - Router tools | Port{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section id="overview">
|
||||
<center>
|
||||
{% if error %}
|
||||
<div class="graph-container">
|
||||
<h2 style="color: red;"><strong>Error:</strong> Interface <strong>{{ interface_name }}</strong> not found.</h2>
|
||||
</div>
|
||||
{% else %}
|
||||
<p><b>Stats for port: {{ interface_name }}</b></p>
|
||||
<div class="graph-container">
|
||||
<p>24H</p>
|
||||
<img src="data:image/svg+xml;base64,{{ daily }}" alt="Day">
|
||||
<p>Week</p>
|
||||
<img src="data:image/svg+xml;base64,{{ weekly }}" alt="Week">
|
||||
<p>Month</p>
|
||||
<img src="data:image/svg+xml;base64,{{ monthly }}" alt="Month">
|
||||
</div>
|
||||
{% endif %}
|
||||
</center>
|
||||
</section>
|
||||
{% endblock %}
|
||||
62
templates/stats.html
Normal file
62
templates/stats.html
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}AS215085 - Router tools | Akvorado{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<script src="../static/js/chart.js"></script>
|
||||
<script src="../static/js/pages/akvorado.js"></script>
|
||||
<script src="../static/js/chartjs-adapter-date-fns"></script>
|
||||
<section id="pie-charts">
|
||||
<div class="stats-overview-container">
|
||||
<h2>Overview</h2>
|
||||
<div id="stats-container" style="text-align: right;">
|
||||
<p><span id="flowRate">Loading...</span> Flows/s</p>
|
||||
<p><span id="exporterCount">Loading...</span> Exporters</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="achart-container">
|
||||
<div class="achart-wrapper" id="src-as-chart">
|
||||
<h5>Top source AS</h5>
|
||||
<canvas id="pieChart1"></canvas>
|
||||
<div id="legend1" class="alegend"></div>
|
||||
</div>
|
||||
<div class="achart-wrapper" id="source-ports-chart">
|
||||
<h5>Top source ports</h5>
|
||||
<canvas id="pieChart2"></canvas>
|
||||
<div id="legend2" class="alegend"></div>
|
||||
</div>
|
||||
<div class="achart-wrapper" id="protocols-chart">
|
||||
<h5>Top protocols</h5>
|
||||
<canvas id="pieChart3"></canvas>
|
||||
<div id="legend3" class="alegend"></div>
|
||||
</div>
|
||||
<div class="achart-wrapper" id="source-countries-chart">
|
||||
<h5>Top source countries</h5>
|
||||
<canvas id="pieChart4"></canvas>
|
||||
<div id="legend4" class="alegend"></div>
|
||||
</div>
|
||||
<div class="achart-wrapper" id="source-etype-chart">
|
||||
<h5>IPv4/IPv6</h5>
|
||||
<canvas id="pieChart5"></canvas>
|
||||
<div id="legend5" class="alegend"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="graph">
|
||||
<h2>Flow graph</h2>
|
||||
<div class="achart-wrapper-chart">
|
||||
<canvas id="liveChart"></canvas>
|
||||
</div>
|
||||
</section>
|
||||
<section id="graph-librenms">
|
||||
<h2>Interface graph</h2>
|
||||
<div>
|
||||
<center><p><b>24H LAN interface {{ interface_name }}</b></p>
|
||||
{% if error %}
|
||||
<h2 style="color: red;"><strong>Error:</strong> Interface <strong>{{ interface_name }}</strong> not found.</h2></center>
|
||||
{% else %}
|
||||
<img src="data:image/svg+xml;base64,{{ daily }}" alt="Day"></center>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
Loading…
Add table
Add a link
Reference in a new issue