mirror of
https://github.com/eduardogsilva/wireguard_webadmin.git
synced 2025-03-18 18:14:02 +00:00
Add RRDTool integration for monitoring WireGuard stats
Introduces a new RRDTool service for tracking WireGuard interface and peer statistics. Adds required Docker configuration, API key handling, and new scripts for managing RRD files. Updates the `entrypoint.sh` and API permissions to accommodate the new functionality.
This commit is contained in:
parent
7ecf111fbe
commit
07a806a073
2
.gitignore
vendored
2
.gitignore
vendored
@ -6,3 +6,5 @@
|
||||
*.pyd
|
||||
wireguard_webadmin/production_settings.py
|
||||
.idea/
|
||||
containers/*/.venv
|
||||
.env
|
@ -25,6 +25,8 @@ def get_api_key(api_name):
|
||||
api_file_path = '/etc/wireguard/api_key'
|
||||
elif api_name == 'routerfleet':
|
||||
api_file_path = '/etc/wireguard/routerfleet_key'
|
||||
elif api_name == 'rrdkey':
|
||||
api_file_path = '/app_secrets/rrdtool_key'
|
||||
else:
|
||||
return api_key
|
||||
|
||||
@ -126,6 +128,12 @@ def wireguard_status(request):
|
||||
pass
|
||||
else:
|
||||
return HttpResponseForbidden()
|
||||
elif request.GET.get('rrdkey'):
|
||||
api_key = get_api_key('rrdkey')
|
||||
if api_key and api_key == request.GET.get('rrdkey'):
|
||||
pass
|
||||
else:
|
||||
return HttpResponseForbidden()
|
||||
else:
|
||||
return HttpResponseForbidden()
|
||||
|
||||
|
17
containers/rrdtool/Dockerfile-rrdtool
Normal file
17
containers/rrdtool/Dockerfile-rrdtool
Normal file
@ -0,0 +1,17 @@
|
||||
FROM python:3.12-slim
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
net-tools \
|
||||
inetutils-ping \
|
||||
inetutils-traceroute \
|
||||
nano \
|
||||
vim-nox \
|
||||
rrdtool \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
COPY wgrrd.py .
|
||||
|
||||
CMD ["python", "-u", "wgrrd.py"]
|
5
containers/rrdtool/requirements.txt
Normal file
5
containers/rrdtool/requirements.txt
Normal file
@ -0,0 +1,5 @@
|
||||
certifi==2025.1.31
|
||||
charset-normalizer==3.4.1
|
||||
idna==3.10
|
||||
requests==2.32.3
|
||||
urllib3==2.3.0
|
233
containers/rrdtool/wgrrd.py
Executable file
233
containers/rrdtool/wgrrd.py
Executable file
@ -0,0 +1,233 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import sys
|
||||
import uuid
|
||||
import base64
|
||||
import time
|
||||
import requests
|
||||
import subprocess
|
||||
|
||||
# Global variables
|
||||
DEBUG = True
|
||||
API_ADDRESS = "http://127.0.0.1:8000"
|
||||
|
||||
# Base directory for storing RRD files
|
||||
RRD_DATA_PATH = "/rrd_data"
|
||||
RRD_PEERS_PATH = os.path.join(RRD_DATA_PATH, "peers")
|
||||
RRD_WGINSTANCES_PATH = os.path.join(RRD_DATA_PATH, "wginstances")
|
||||
|
||||
|
||||
def debug_log(message):
|
||||
"""
|
||||
Prints a debug message with a timestamp if DEBUG is enabled.
|
||||
Timestamp format is similar to syslog (e.g. "Mar 10 14:55:02").
|
||||
"""
|
||||
if DEBUG:
|
||||
timestamp = time.strftime("%b %d %H:%M:%S")
|
||||
print(f"{timestamp} - DEBUG - {message}")
|
||||
|
||||
|
||||
def get_api_key():
|
||||
"""
|
||||
Reads the API key from a file and validates it as a UUID.
|
||||
Returns the API key as a string if valid, otherwise returns None.
|
||||
"""
|
||||
api_key = None
|
||||
api_file_path = "/app_secrets/rrdtool_key"
|
||||
|
||||
if os.path.exists(api_file_path) and os.path.isfile(api_file_path):
|
||||
with open(api_file_path, 'r') as file:
|
||||
key_content = file.read().strip()
|
||||
try:
|
||||
uuid_obj = uuid.UUID(key_content)
|
||||
if str(uuid_obj) == key_content:
|
||||
api_key = str(uuid_obj)
|
||||
except Exception as e:
|
||||
debug_log(f"Error validating API key: {e}")
|
||||
return api_key
|
||||
|
||||
|
||||
def create_peer_rrd(rrd_file):
|
||||
"""
|
||||
Creates an RRD file for a peer with 3 data sources:
|
||||
- tx: COUNTER
|
||||
- rx: COUNTER
|
||||
- status: GAUGE
|
||||
"""
|
||||
cmd = [
|
||||
"rrdtool", "create", rrd_file,
|
||||
"--step", "300",
|
||||
"DS:tx:COUNTER:600:0:U",
|
||||
"DS:rx:COUNTER:600:0:U",
|
||||
"DS:status:GAUGE:600:0:1",
|
||||
"RRA:AVERAGE:0.5:1:1440",
|
||||
"RRA:AVERAGE:0.5:6:700",
|
||||
"RRA:AVERAGE:0.5:24:775",
|
||||
"RRA:AVERAGE:0.5:288:797"
|
||||
]
|
||||
try:
|
||||
subprocess.run(cmd, check=True)
|
||||
debug_log(f"Created peer RRD file: {rrd_file}")
|
||||
except subprocess.CalledProcessError as e:
|
||||
debug_log(f"Error creating peer RRD file: {rrd_file} {e}")
|
||||
|
||||
|
||||
def create_instance_rrd(rrd_file):
|
||||
"""
|
||||
Creates an RRD file for a wireguard instance with 2 data sources:
|
||||
- tx: COUNTER
|
||||
- rx: COUNTER
|
||||
"""
|
||||
cmd = [
|
||||
"rrdtool", "create", rrd_file,
|
||||
"--step", "300",
|
||||
"DS:tx:COUNTER:600:0:U",
|
||||
"DS:rx:COUNTER:600:0:U",
|
||||
"RRA:AVERAGE:0.5:1:1440",
|
||||
"RRA:AVERAGE:0.5:6:700",
|
||||
"RRA:AVERAGE:0.5:24:775",
|
||||
"RRA:AVERAGE:0.5:288:797"
|
||||
]
|
||||
try:
|
||||
subprocess.run(cmd, check=True)
|
||||
debug_log(f"Created instance RRD file: {rrd_file}")
|
||||
except subprocess.CalledProcessError as e:
|
||||
debug_log(f"Error creating instance RRD file: {rrd_file} {e}")
|
||||
|
||||
|
||||
def process_peer(peer_key, peer_data):
|
||||
"""
|
||||
Processes a single peer:
|
||||
- Converts the peer key to a URL-safe base64 string (removing any '=' characters)
|
||||
- Constructs the RRD file path for the peer (stored in RRD_PEERS_PATH)
|
||||
- Extracts the transfer data (tx and rx) from the peer data
|
||||
- Determines host status (1 for online, 0 for offline) based on the latest-handshakes value
|
||||
- Creates the RRD file if it does not exist
|
||||
- Executes the rrdtool command to update the RRD database with tx, rx, and status
|
||||
"""
|
||||
# Convert peer_key to URL-safe base64 and remove '=' characters
|
||||
b64_peer = base64.urlsafe_b64encode(peer_key.encode()).decode().replace("=", "")
|
||||
# Define the peer RRD file path
|
||||
rrd_file = os.path.join(RRD_PEERS_PATH, f"{b64_peer}.rrd")
|
||||
|
||||
# Create the RRD file if it doesn't exist
|
||||
if not os.path.exists(rrd_file):
|
||||
create_peer_rrd(rrd_file)
|
||||
|
||||
# Extract transfer data (tx and rx)
|
||||
tx = peer_data.get("transfer", {}).get("tx", 0)
|
||||
rx = peer_data.get("transfer", {}).get("rx", 0)
|
||||
|
||||
# Determine host status based on latest-handshakes.
|
||||
# Host is offline (0) if latest-handshakes is "0" or if more than 5 minutes (300s) have passed.
|
||||
latest_handshake = peer_data.get("latest-handshakes", "0")
|
||||
try:
|
||||
last_time = int(latest_handshake)
|
||||
except ValueError:
|
||||
last_time = 0
|
||||
|
||||
current_time = int(time.time())
|
||||
status = 0 if (last_time == 0 or (current_time - last_time) > 300) else 1
|
||||
|
||||
# Build the update command for rrdtool (syntax: "N:<tx>:<rx>:<status>")
|
||||
update_str = f"N:{tx}:{rx}:{status}"
|
||||
cmd = ["rrdtool", "update", rrd_file, update_str]
|
||||
|
||||
debug_log("Executing peer update command: " + " ".join(cmd))
|
||||
|
||||
try:
|
||||
subprocess.run(cmd, check=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
debug_log(f"Error updating RRD for peer {peer_key} (file {rrd_file}): {e}")
|
||||
|
||||
|
||||
def update_instance(interface, total_tx, total_rx):
|
||||
"""
|
||||
Updates the RRD file for a wireguard instance corresponding to an interface.
|
||||
The file is stored in RRD_WGINSTANCES_PATH with the name <interface>.rrd.
|
||||
If the file does not exist, it will be created.
|
||||
The update command is: "N:<total_tx>:<total_rx>".
|
||||
"""
|
||||
instance_file = os.path.join(RRD_WGINSTANCES_PATH, f"{interface}.rrd")
|
||||
|
||||
# Create the instance RRD file if it doesn't exist
|
||||
if not os.path.exists(instance_file):
|
||||
create_instance_rrd(instance_file)
|
||||
|
||||
update_str = f"N:{total_tx}:{total_rx}"
|
||||
cmd = ["rrdtool", "update", instance_file, update_str]
|
||||
|
||||
debug_log("Executing instance update command: " + " ".join(cmd))
|
||||
|
||||
try:
|
||||
subprocess.run(cmd, check=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
debug_log(f"Error updating RRD for instance {interface} (file {instance_file}): {e}")
|
||||
|
||||
|
||||
def main_loop():
|
||||
"""
|
||||
Main loop that:
|
||||
- Ensures the necessary directories exist
|
||||
- Retrieves the API key (terminating the script if invalid)
|
||||
- Queries the wireguard status API every 5 minutes
|
||||
- Processes each peer using process_peer()
|
||||
- Aggregates total tx and rx per interface and updates the corresponding instance RRD
|
||||
- Waits until 5 minutes have passed since the beginning of the loop before restarting
|
||||
"""
|
||||
# Ensure directories exist
|
||||
os.makedirs(RRD_PEERS_PATH, exist_ok=True)
|
||||
os.makedirs(RRD_WGINSTANCES_PATH, exist_ok=True)
|
||||
|
||||
# Retrieve API key before entering the loop; exit if invalid.
|
||||
api_key = get_api_key()
|
||||
if not api_key:
|
||||
print("API key not found or invalid. Exiting.")
|
||||
sys.exit(1)
|
||||
|
||||
while True:
|
||||
loop_start = time.time()
|
||||
# Refresh the API key on every iteration in case the file changes
|
||||
api_key = get_api_key()
|
||||
if not api_key:
|
||||
print("API key not found or invalid. Exiting.")
|
||||
sys.exit(1)
|
||||
|
||||
# Build the URL for the API call
|
||||
url = f"{API_ADDRESS}/api/wireguard_status/?rrdkey={api_key}"
|
||||
debug_log("Querying API at: " + url)
|
||||
try:
|
||||
response = requests.get(url, timeout=10)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
except Exception as e:
|
||||
debug_log("Error fetching or parsing API data: " + str(e))
|
||||
elapsed = time.time() - loop_start
|
||||
time.sleep(max(300 - elapsed, 0))
|
||||
continue
|
||||
|
||||
# Process each interface and its peers, aggregate tx and rx for the interface
|
||||
for interface, peers in data.items():
|
||||
debug_log(f"Processing interface: {interface}")
|
||||
total_tx = 0
|
||||
total_rx = 0
|
||||
for peer_key, peer_info in peers.items():
|
||||
process_peer(peer_key, peer_info)
|
||||
# Sum the tx and rx values for this interface
|
||||
tx = peer_info.get("transfer", {}).get("tx", 0)
|
||||
rx = peer_info.get("transfer", {}).get("rx", 0)
|
||||
total_tx += tx
|
||||
total_rx += rx
|
||||
|
||||
# Update the RRD for the wireguard instance with aggregated values
|
||||
update_instance(interface, total_tx, total_rx)
|
||||
|
||||
# Calculate elapsed time and wait the remaining time to complete 5 minutes
|
||||
elapsed = time.time() - loop_start
|
||||
sleep_time = max(300 - elapsed, 0)
|
||||
debug_log(f"Waiting {sleep_time:.2f} seconds until next execution.")
|
||||
time.sleep(sleep_time)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main_loop()
|
@ -8,12 +8,14 @@ services:
|
||||
environment:
|
||||
- SERVER_ADDRESS=127.0.0.1
|
||||
- DEBUG_MODE=True
|
||||
- COMPOSE_VERSION=02b
|
||||
- COMPOSE_VERSION=02r
|
||||
volumes:
|
||||
- wireguard:/etc/wireguard
|
||||
- static_volume:/app_static_files/
|
||||
- .:/app
|
||||
- dnsmasq_conf:/etc/dnsmasq
|
||||
- app_secrets:/app_secrets/
|
||||
- rrd_data:/rrd_data/
|
||||
ports:
|
||||
# Do not directly expose the Django port to the internet, use some kind of reverse proxy with SSL.
|
||||
- "127.0.0.1:8000:8000"
|
||||
@ -40,6 +42,18 @@ services:
|
||||
depends_on:
|
||||
- wireguard-webadmin
|
||||
|
||||
wireguard-webadmin-rrdtool:
|
||||
container_name: wireguard-webadmin-rrdtool
|
||||
restart: unless-stopped
|
||||
build:
|
||||
context: ./containers/rrdtool
|
||||
dockerfile: Dockerfile-rrdtool
|
||||
volumes:
|
||||
- app_secrets:/app_secrets/
|
||||
- rrd_data:/rrd_data/
|
||||
depends_on:
|
||||
- wireguard-webadmin
|
||||
|
||||
wireguard-webadmin-dns:
|
||||
container_name: wireguard-webadmin-dns
|
||||
restart: unless-stopped
|
||||
@ -52,4 +66,6 @@ services:
|
||||
volumes:
|
||||
static_volume:
|
||||
wireguard:
|
||||
dnsmasq_conf:
|
||||
dnsmasq_conf:
|
||||
app_secrets:
|
||||
rrd_data:
|
@ -2,7 +2,7 @@
|
||||
|
||||
set -e
|
||||
|
||||
if [[ "$COMPOSE_VERSION" != "02b" ]]; then
|
||||
if [[ "$COMPOSE_VERSION" != "02r" ]]; then
|
||||
echo "ERROR: Please upgrade your docker compose file. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
@ -17,6 +17,10 @@ if [[ "${DEBUG_MODE,,}" == "true" ]]; then
|
||||
DEBUG_VALUE="True"
|
||||
fi
|
||||
|
||||
if [ ! -f /app_secrets/rrdtool_key ]; then
|
||||
cat /proc/sys/kernel/random/uuid > /app_secrets/rrdtool_key
|
||||
fi
|
||||
|
||||
cat > /app/wireguard_webadmin/production_settings.py <<EOL
|
||||
DEBUG = $DEBUG_VALUE
|
||||
ALLOWED_HOSTS = ['wireguard-webadmin', '$SERVER_ADDRESS']
|
||||
|
@ -28,7 +28,7 @@ from dns.views import view_static_host_list, view_manage_static_host, view_manag
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
# path('admin/', admin.site.urls),
|
||||
path('', view_welcome, name='welcome'),
|
||||
path('status/', view_wireguard_status, name='wireguard_status'),
|
||||
path('dns/', view_static_host_list, name='static_host_list'),
|
||||
|
Loading…
Reference in New Issue
Block a user