This commit is contained in:
Blackwhitebear8 2025-06-22 16:33:28 +02:00
commit 5e31dd0214
37 changed files with 2082 additions and 0 deletions

12
.env Normal file
View 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
View 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
View 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
View file

17
modules/akvorado.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because one or more lines are too long

248
static/css/style.css Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
static/img/background.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

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

File diff suppressed because one or more lines are too long

14
static/js/chart.js Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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
View 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
View 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");
});
}

View 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
View 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");
});
}

View 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");
});
}

View 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
View 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
View 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
View 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 PixelHostings 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>&copy; 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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 %}