feat: WireGuard metrics collector + integration test stack
Metrics collector (wiregui/collector.py): - Standalone process spawned by web app when WG_METRICS_ENABLED=true - Polls wg show dump every WG_METRICS_POLL_INTERVAL seconds (default 5) - Updates device stats in PostgreSQL - Pushes Prometheus-format metrics to VictoriaMetrics (if configured) - Graceful shutdown on SIGTERM Integration test stack (compose.yml): - Unified compose file for dev, test, and integration modes - VictoriaMetrics single-node TSDB for metrics storage - 3 mock WireGuard client containers generating ping traffic - Automated setup script seeds server keypair, admin user, client devices - make test-stack-up: one command to start everything - make test-stack-verify: validates metrics flowing end-to-end Infrastructure: - Makefile with targets for dev, test, integration, and production - Integration tests verify VictoriaMetrics has data for all 3 clients - Fix Dockerfile to include img/ directory - Separate TESTS.md for test tracking, clean TODO.md for features only
This commit is contained in:
parent
70eb9f6b12
commit
c5b66349d6
16 changed files with 932 additions and 115 deletions
4
docker/mock-clients/Dockerfile
Normal file
4
docker/mock-clients/Dockerfile
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
FROM alpine:3.20
|
||||
RUN apk add --no-cache wireguard-tools iproute2 iputils-ping
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
27
docker/mock-clients/entrypoint.sh
Executable file
27
docker/mock-clients/entrypoint.sh
Executable file
|
|
@ -0,0 +1,27 @@
|
|||
#!/bin/sh
|
||||
# WireGuard mock client — configures interface and generates traffic
|
||||
set -e
|
||||
|
||||
echo "[*] Configuring WireGuard interface..."
|
||||
ip link add wg0 type wireguard
|
||||
wg setconf wg0 /etc/wireguard/wg0.conf
|
||||
ip address add "${CLIENT_IP}/32" dev wg0
|
||||
ip link set wg0 up
|
||||
|
||||
# Route traffic to the VPN subnet through the tunnel
|
||||
ip route add 10.3.2.0/24 dev wg0
|
||||
|
||||
echo "[*] WireGuard client up: ${CLIENT_IP}"
|
||||
echo "[*] Generating traffic to peers every ${PING_INTERVAL:-5}s..."
|
||||
|
||||
while true; do
|
||||
# Ping the server (first host in the subnet)
|
||||
ping -c 1 -W 1 10.3.2.1 > /dev/null 2>&1 || true
|
||||
|
||||
# Ping other peers if specified
|
||||
for peer_ip in $PEER_IPS; do
|
||||
ping -c 1 -W 1 "$peer_ip" > /dev/null 2>&1 || true
|
||||
done
|
||||
|
||||
sleep "${PING_INTERVAL:-5}"
|
||||
done
|
||||
147
docker/mock-clients/setup.py
Normal file
147
docker/mock-clients/setup.py
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Seed the test stack: generate server + client keypairs, write client WG configs,
|
||||
and insert devices into the database.
|
||||
|
||||
Usage: uv run python docker/mock-clients/setup.py
|
||||
|
||||
Requires: Postgres running and migrations applied (alembic upgrade head).
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from uuid import uuid4
|
||||
|
||||
from sqlmodel import select
|
||||
|
||||
from wiregui.auth.passwords import hash_password
|
||||
from wiregui.db import async_session, engine
|
||||
from wiregui.models.configuration import Configuration
|
||||
from wiregui.models.device import Device
|
||||
from wiregui.models.user import User
|
||||
from wiregui.utils.crypto import generate_keypair, generate_preshared_key
|
||||
|
||||
NUM_CLIENTS = 3
|
||||
SUBNET = "10.3.2"
|
||||
SERVER_ENDPOINT = "wiregui:51820"
|
||||
CONFIG_DIR = Path(__file__).parent / "configs"
|
||||
|
||||
# Test admin user
|
||||
ADMIN_EMAIL = "admin@test.local"
|
||||
ADMIN_PASSWORD = "admin123"
|
||||
|
||||
# Client definitions
|
||||
CLIENTS = [
|
||||
{"name": f"test-client-{i}", "ip": f"{SUBNET}.{100 + i}"}
|
||||
for i in range(1, NUM_CLIENTS + 1)
|
||||
]
|
||||
|
||||
|
||||
async def seed():
|
||||
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
async with async_session() as session:
|
||||
# --- Server keypair ---
|
||||
config = (await session.execute(select(Configuration).limit(1))).scalar_one_or_none()
|
||||
if config is None:
|
||||
config = Configuration()
|
||||
session.add(config)
|
||||
await session.flush()
|
||||
|
||||
if not config.server_private_key or not config.server_public_key:
|
||||
server_priv, server_pub = generate_keypair()
|
||||
config.server_private_key = server_priv
|
||||
config.server_public_key = server_pub
|
||||
session.add(config)
|
||||
print(f" Server keypair generated: {server_pub[:20]}...")
|
||||
else:
|
||||
server_pub = config.server_public_key
|
||||
print(f" Server keypair already exists: {server_pub[:20]}...")
|
||||
|
||||
# --- Admin user ---
|
||||
admin = (await session.execute(
|
||||
select(User).where(User.email == ADMIN_EMAIL)
|
||||
)).scalar_one_or_none()
|
||||
if admin is None:
|
||||
admin = User(
|
||||
email=ADMIN_EMAIL,
|
||||
password_hash=hash_password(ADMIN_PASSWORD),
|
||||
role="admin",
|
||||
)
|
||||
session.add(admin)
|
||||
await session.flush()
|
||||
print(f" Admin user created: {ADMIN_EMAIL}")
|
||||
else:
|
||||
print(f" Admin user already exists: {ADMIN_EMAIL}")
|
||||
|
||||
# --- Client devices (delete + recreate for clean state) ---
|
||||
client_names = [c["name"] for c in CLIENTS]
|
||||
client_ips = [c["ip"] for c in CLIENTS]
|
||||
stale = (await session.execute(
|
||||
select(Device).where(
|
||||
Device.name.in_(client_names) | Device.ipv4.in_(client_ips)
|
||||
)
|
||||
)).scalars().all()
|
||||
for d in stale:
|
||||
await session.delete(d)
|
||||
if stale:
|
||||
await session.flush()
|
||||
print(f" Cleaned up {len(stale)} stale device(s)")
|
||||
|
||||
for client in CLIENTS:
|
||||
client_priv, client_pub = generate_keypair()
|
||||
psk = generate_preshared_key()
|
||||
|
||||
device = Device(
|
||||
name=client["name"],
|
||||
public_key=client_pub,
|
||||
preshared_key=psk,
|
||||
ipv4=client["ip"],
|
||||
user_id=admin.id,
|
||||
)
|
||||
session.add(device)
|
||||
|
||||
client["privkey"] = client_priv
|
||||
client["pubkey"] = client_pub
|
||||
client["psk"] = psk
|
||||
print(f" Device '{client['name']}' created ({client['ip']})")
|
||||
|
||||
await session.commit()
|
||||
|
||||
# --- Write client WG configs ---
|
||||
for i, client in enumerate(CLIENTS):
|
||||
conf = f"""[Interface]
|
||||
PrivateKey = {client["privkey"]}
|
||||
|
||||
[Peer]
|
||||
PublicKey = {server_pub}
|
||||
PresharedKey = {client["psk"]}
|
||||
Endpoint = {SERVER_ENDPOINT}
|
||||
AllowedIPs = {SUBNET}.0/24
|
||||
PersistentKeepalive = 5
|
||||
"""
|
||||
conf_path = CONFIG_DIR / f"client{i + 1}.conf"
|
||||
conf_path.write_text(conf)
|
||||
print(f" Config written: {conf_path}")
|
||||
|
||||
# --- Write env vars for compose ---
|
||||
env_lines = []
|
||||
for i, client in enumerate(CLIENTS):
|
||||
other_ips = " ".join(c["ip"] for c in CLIENTS if c["ip"] != client["ip"])
|
||||
env_lines.append(f"CLIENT{i + 1}_IP={client['ip']}")
|
||||
env_lines.append(f"CLIENT{i + 1}_PEERS={other_ips}")
|
||||
env_path = CONFIG_DIR / "clients.env"
|
||||
env_path.write_text("\n".join(env_lines) + "\n")
|
||||
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
def main():
|
||||
print("[*] Seeding test stack...")
|
||||
asyncio.run(seed())
|
||||
print("\n[*] Done. Start the stack with:")
|
||||
print(" make test-stack-up")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue