Compare commits

..

4 Commits

41 changed files with 2707 additions and 310 deletions
+16
View File
@@ -0,0 +1,16 @@
__pycache__/
*.pyc
*.pyo
*.pyd
*.log
venv/
.env
.env.*
.DS_Store
Thumbs.db
ansible_workspace/staging/
ansible_workspace/network_tool_backup_*.tar.gz
openvpn/runtime/
.git
.gitmodules
.gitignore
+16
View File
@@ -0,0 +1,16 @@
# Flask / Gunicorn
FLASK_SECRET_KEY=change-this
CORE_API_USER=admin
CORE_API_PASS=Super4dmin!
# Dashboard credentials for ComboCore portal access
DASHBOARD_USER=admin@hpe.com
DASHBOARD_PASSWORD=kxly7o6FboYUoQXSeLrs2xodHIeybQRwcMs33QC5#
# Dashboard environments (JSON object mapping name -> URL)
DASHBOARD_ENVIRONMENTS={"Production":"https://dashboard.private5g.networking.hpe.com","Test":"https://your-test-dashboard-url.com"}
# VPN runtime
VPN_CONFIG_DIR=/vpn/configs
VPN_RUNTIME_DIR=/vpn/runtime
VPN_CONFIG_NAMES=Triton,Star,Bluebonnet,Lonestar,Production,US-Support,EU-Support
+37
View File
@@ -0,0 +1,37 @@
FROM python:3.11-slim
ENV PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
FLASK_ENV=production \
VPN_CONFIG_DIR=/vpn/configs \
VPN_RUNTIME_DIR=/vpn/runtime
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
dumb-init \
openvpn \
iproute2 \
iputils-ping \
net-tools \
openssh-client \
ansible \
sshpass \
jq \
curl \
tzdata \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt /tmp/requirements.txt
RUN pip install --upgrade pip \
&& pip install -r /tmp/requirements.txt
COPY . /app
RUN mkdir -p /vpn/configs /vpn/runtime
EXPOSE 8000
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
CMD ["gunicorn", "--timeout", "300", "--bind", "0.0.0.0:8000", "app:app"]
-90
View File
@@ -1,90 +0,0 @@
client
remote vpn.arubaedge-triton.athonetusa.com 1091
comp-lzo yes
dev tun
proto udp
nobind
script-security 2
persist-key
persist-tun
<cert>
-----BEGIN CERTIFICATE-----
MIIDWDCCAkCgAwIBAgIQCRnQJil5kyTX9cW/Oc6KwjANBgkqhkiG9w0BAQsFADAW
MRQwEgYDVQQDDAtFYXN5LVJTQSBDQTAeFw0yMzExMTAxNTE0NDVaFw0yNjAyMTIx
NTE0NDVaMBUxEzARBgNVBAMMCnJhcy1jbGllbnQwggEiMA0GCSqGSIb3DQEBAQUA
A4IBDwAwggEKAoIBAQC9usyHF2gN/Vbcq8VzHG8YjdZH8ffnKfcCDlScn6QShSJU
U/Vvt9e95XgnNNf3CV16kgwccHltTIDsnQ0xIg6slKZe9199O9jW5FMbgsqyHr17
d31/r2dnDrGCwqzW2J8GruGAfGnORrP7yyXbtPAg9Xo6dSNAJP2LKPNBSAgC1qJX
zaU4abqTu1S9bHtZbdBM5Gu44IEq4OmOjzhkK/HDUIdxsW4M2XDmwS+LEEdJrQzX
QfPuy0fIqG6m64yMj8KqE3UJudq/ZPvBTMicEwQtiEH0ZPoVR8mUOA6EbdLcPOgD
zVnTtoDI/g87SRk8akec00U+TkFfDwtejhaun1WLAgMBAAGjgaIwgZ8wCQYDVR0T
BAIwADAdBgNVHQ4EFgQUzmKlGzqAyAPWF3/dP8nyhIE8d40wUQYDVR0jBEowSIAU
LPMtzvN7A0qEAbbfjHUxACs/x8ahGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENB
ghQKCs3AEEY/UWj6kXXTA8+2HtC1vTATBgNVHSUEDDAKBggrBgEFBQcDAjALBgNV
HQ8EBAMCB4AwDQYJKoZIhvcNAQELBQADggEBACZkv79VL5TMQRvJ6WWOvqihweLE
BAmVFyZfwD/y2biTPeQTpojkJs3XMXIRvluCxihpMkAS73d2bzx1HZPQm0dF2Szb
/iHLgMmUbeInaT78pFpCu+4Va6YIUcFM2jqRLkg4Aa+bzFtT1hG8TkXOu7VvPI8l
4fSKnRSN77uHEQp6KWd+oDMUqtjSzo3Lc0g9LU5Ex0p0z0Cx7wihLXLJsDRFxp8G
EMU4apula0lHCd8fc5sIQarweybY9CM0Vymkes4FmQr/1yWQwucdC8CTDvAR3X2m
GfLIyNhvHQKMzJKEpGrj4tMqLNZrnwuTvrrLbfIH/B+6w7f4bI3Il/m/8LU=
-----END CERTIFICATE-----
</cert>
<key>
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIFHDBOBgkqhkiG9w0BBQ0wQTApBgkqhkiG9w0BBQwwHAQI0YGeflzHjMQCAggA
MAwGCCqGSIb3DQIJBQAwFAYIKoZIhvcNAwcECOMCCAht/lqeBIIEyNabZEOIhXYp
m5Jn69rv65Ififg0Tar7kBmyll5KHGP7KfXM8wtT8JvYyNYceEi78aO+/pAF1woz
Yd0EmC/BrvmhtWuvSJXqs3CnEZk3vO3jXTsnWYzQSHs8I/Os/AZSY8jr0ZYlaiiO
PcnSNyejnU3JFCI8LJam2soqqpKyoSNGGoRSVW9+EK8fz4cGmNdtWnHR7TwwCRn0
mczVWEhX9x8OmpNdYtD9ZCCQ369VkoSE+11zXvUkoTtMmMIG59M8F7s4jvdmGJGz
P8JDAVaIj3Cl7muvN1ob51UBlIciEQxIa3ozApJutHBNP4nnvI8jRJa9uspajVQR
T/lKp5vmUc17cSLG9CTmJtBEvJ3tdfPq34C9n3kEwau9nAmp0Hoh/4axfhWfIVnY
vgXI5zkvDm98DNHNjy0Ic6IttJPeDkTR9DfuIjcPehdPd5x1s/Hcc63VwtzceZlu
dLtOWnceEfqJkLIfIheXDaJdPgEknpH3FX8iKMY2F1WeSBBjYBQWkJq7Gg6ULmow
bHg/k0Ah0KQo6m7uhoV8r7F29NOwuHAnguCdKwHXYdQJDJNP5Yr7HtqUiMqPHYSS
IeWH4+h3Rmug/A5pexwePddlHAXcFFTPbMjRkpXPHWUXoBbBad7JtJgTBKV14ZnZ
1u/nMCEQ22oU7BXIA+B0BBl4HdeotOwD7Ocbqgf1d0fgYoUbgPxOLYk2kvOM93EP
GRqUtkTGOzBi7IIINQbRKLAQJFAb62XxRXv3tKGDR7y4H6IdmMVcbGzSIIeKNppS
yIQadpi/O23qhgD+cp3dhIlpnXe0JvoN9JdQE0wfhkhpScBv7XIy5mtuidOVqUl4
pXFipjRUKaLy0qKY43wxxUQXfHVKGYi8ubfGCgeDPD0wNFSk5qfDgOPxuzE/L7Fc
scjJKZ9rAaJ+SpbC2GC8DujFBZyaLSCi3HZS6cpVSucAhUyJnKiT6YEEaFakLMeT
E7GYX6upuMoXBY+Km0Dz9pu+PwJVTjohzc4NmBfIAUz+eM/Mi42MoP0nnhStVR9D
UhvWx3bVRC38Pzh6Zg34/1BREfdPAuYvG1VXSe1zZ6Ak12txAy9YxFVdqVAsNjT1
zmWMYFzXkWraQlhXkGBCWgeevLrB2Hmu4aeLesXBvY1qV1v57qSWuAksr0wvSdL+
wfol/6JRLJfSt0uyO04CpE0rh/T+pwpRBLogH8XqUYzZtJq9SdQEH8ObKgg/Yx8p
p+7pQYlLVZtJVJueiTjqaE3rZ46oT8FHpyQUkubHtiQB5P7mPRn2u0UezUUbThei
SWcxKfES1laP7MBRiUspmVxT/JR7WZ5RV9mm6AWo8FZ1bWo0Fy7yCaYKR6xkH2W8
bEmlZvPbS0twO9rem5CuOrDZtfevQt7PQm6cQ/GXh3XpUOOl11acce3KNK/xfatG
UOueLLJwHEGP+wB3/5QRwrvQ3t59mwCph5pbM+f5wIhNUJwBDGVm95JzMVxj1oCg
ROCrChFkioo/TXnWoHl6TPqqUf/fFDv/nZPnwos4qY9jmuepYZWtXYmH4hrBcvul
h/05mBmjYrE+LnoehIikjEpsixaryyMalF947tCGJOlgLecth2PlxgguVaEhuKVR
Q9ESsikOaNTCn8UScfQzBw==
-----END ENCRYPTED PRIVATE KEY-----
</key>
<ca>
-----BEGIN CERTIFICATE-----
MIIDSzCCAjOgAwIBAgIUCgrNwBBGP1Fo+pF10wPPth7Qtb0wDQYJKoZIhvcNAQEL
BQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMjMxMTEwMTUxMzE0WhcNMzMx
MTA3MTUxMzE0WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcN
AQEBBQADggEPADCCAQoCggEBAJT1bO4fs5sIDOsVecr9vY0VomMqzRLbbVN0lBdo
qxdSOXHEosJ6qZSJoOb/XIIj0828NQQc7TBUhzZsRDn4oyopQ0wIXPN5hgmx9kOm
pGhdiy3boyjpoXgoHUd2CGlIJJNneXs3OqfxM3NjuLkfxwbX7SuU5r6KXEszbyA0
0CpGm6YKLshkVO4QbVG82A0+KkqRhGdk7pddsvXhtaHyz7OsUI3EHg6FoANKAjrl
4SgTDBfbg++iFEaZwst73P4pHcOx3r2zzseNwGEFdPnwXPjQQMxUl3ikaAzsKRhG
4zTiAXkUtBbDUvEXDY0yoG7eyARXANWdYi0pxU86aVQJ0HUCAwEAAaOBkDCBjTAd
BgNVHQ4EFgQULPMtzvN7A0qEAbbfjHUxACs/x8YwUQYDVR0jBEowSIAULPMtzvN7
A0qEAbbfjHUxACs/x8ahGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghQKCs3A
EEY/UWj6kXXTA8+2HtC1vTAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkq
hkiG9w0BAQsFAAOCAQEAb+j9JzhXrP1xY9AtBAhkYLTLfcqICiSXGjRjONngTe82
MFcfJx53ptHk1Xs2GTjv7hshgo4ADhCHfqnGfyj4weoZpwd7VBswqqvuikhCXfpx
NbkI/E2Gk5BK8ThsJbGbNgJg9Vg34V1za00T7lAWwRNdJC/kBnTwI/sdvQ0AYAZy
IYlOCThhpNhtlIiLZ36ebw3recuAgA0YklwH3oMRh+hsFgjcmJg9bx/VcjIapsjO
OnksHxRDMxQs1n2Qd+XC724mHS4eSvJwoIC/WeWX0r3N7X63cRWcRQA83TpGtUAA
rerEaydkWUO+6+HA7FQglxf06VcpgJwtqa6Tm7iDZw==
-----END CERTIFICATE-----
</ca>
@@ -8,5 +8,5 @@ vswitches:
portgroups:
- { vSwitch: GAF_VSWITCH, vlanId: 501, vlanName: GAF_BP_501_OAM }
- { vSwitch: GAF_VSWITCH, vlanId: 502, vlanName: GAF_BP_502_RAN }
- { vSwitch: GAF_VSWITCH, vlanId: 10, vlanName: DN_01 }
- { vSwitch: GAF_VSWITCH, vlanId: 2100, vlanName: DN_01 }
- { vSwitch: GAF_VSWITCH, vlanId: 4095, vlanName: GAF_BP_T_510_515 }
@@ -39,29 +39,65 @@ _ngc_ext_aio_transport:
# RAN transports (use RAN IP)
- action: override_amf_n2_transport
params: { address: 192.168.120.95, vrf: RAN }
params: { address: 10.10.0.2, vrf: RAN }
- action: override_mme_transport
params: { s1_address: 192.168.120.95, s1_vrf: RAN }
params: { s1_address: 10.10.0.2, s1_vrf: RAN }
# UPF transports (N3 on RAN)
- action: override_upf_transport
params:
n3_interface: eth1
n3_address: 192.168.120.95
n3_address: 10.10.0.2
n3_vrf: RAN
# Avoid s-NSSAI on PFCP
- action: set_pfcp_ies
params:
s_nssai: false
# Definition of Network Instances for PFCP
- action: set_pfcp_net_instances
params:
s1_u: RAN
s5s8_u: TELCO
s11_u: TELCO
n3: RAN
n4_u: TELCO
# DNN configuration
_ngc_ext_aio_dnn:
# internet DNN (5G Selection)
- action: add_smf_dnn
params:
dnn: internet
n6_vrf: DN_01
dns:
- 8.8.8.8
# internet DNN (LTE Selection)
- action: add_smf_dnn
params:
dnn: internet
nssai:
sd: ''
sst: 1
n6_vrf: DN_01
dns:
- 8.8.8.8
# DN/DNN (N6) with UE pool
- action: add_n6_dnn
params:
n6_dnn: internet
n6_vrf: DN_01
n6_vlan: 10
n6_vlan: 2100
n6_vrf_table: 511
n6_interface: eth2
n6_ip: 192.168.110.95/24
n6_gw: 192.168.110.1
n6_ip: 10.121.0.150/24
n6_gw: 10.121.0.1
n6_upf_pools:
- upf_route: 100.0.94.0/24
- upf_route: 192.168.4.0/24
nssai: false
n6_bgp:
local_as: 65001
@@ -5,8 +5,8 @@ net_recipe: generic_bgp
oam_network:
add_ansible_host_address: false
addresses:
- 192.168.105.159/24
gateway4: 192.168.105.1
- 10.121.2.94/24
gateway4: 10.121.2.1
# --- NTP ---
ntp:
@@ -27,7 +27,7 @@ _ngc_ext_aio_net:
interface: eth1
vrf: RAN
addresses:
- 192.168.120.95/24 # S1+N2+N3
- 10.10.0.2/24 # S1+N2+N3
routes:
- destination: 0.0.0.0/0
gateway: 192.168.120.1
gateway: 10.10.0.254
+1 -1
View File
@@ -1,7 +1,7 @@
all:
hosts:
GBP08-AIO-1:
ansible_host: 100.93.1.100
ansible_host: 100.93.0.240
children:
ESXi:
hosts:
+155 -16
View File
@@ -1,8 +1,9 @@
from flask import Flask, render_template, request, jsonify, Response
from flask import Flask, render_template, request, jsonify, Response, stream_with_context
import core_functions
import auth_utils
import logging
import os
import json
import urllib3; urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
import requests
from requests.exceptions import HTTPError, RequestException
@@ -17,20 +18,60 @@ from services.remote_admin import (
)
from services.yaml_writer import STAGING, render_to_file
from pathlib import Path
from services import vpn_runtime
from services.log_stream import JournalctlStream, LogTarget
API_USER = os.getenv("CORE_API_USER", "admin")
API_PASS = os.getenv("CORE_API_PASS", "Super4dmin!") # consider moving to env/secret
DASHBOARD_URLS = {
"Triton": "https://dashboard.arubaedge-triton.athonetusa.com",
"Star": "https://dashboard.arubaedge-star.athonetusa.com",
"Bluebonnet": "https://dashboard.arubaedge-bluebonnet.athonetusa.com",
"Lonestar": "https://dashboard.arubaedge-lonestar.athonetusa.com",
"Production": "https://dashboard.us-east-2.p5g.athonet.cloud",
"Test (future)": "https://your-test-dashboard-url.com"
}
def _load_dashboard_urls() -> dict:
default = {
"Production": "https://dashboard.private5g.networking.hpe.com",
"Test": "https://your-test-dashboard-url.com",
}
raw = os.getenv("DASHBOARD_ENVIRONMENTS")
if not raw:
return default
try:
parsed = json.loads(raw)
cleaned = {str(k): str(v) for k, v in parsed.items() if str(v).strip()}
return cleaned or default
except json.JSONDecodeError as exc:
logging.warning("Failed to parse DASHBOARD_ENVIRONMENTS (%s); falling back to defaults", exc)
return default
DASHBOARD_URLS = _load_dashboard_urls()
def _resolve_dashboard_url(name: str) -> str:
if not name:
raise ValueError("Dashboard selection is required")
url = DASHBOARD_URLS.get(name)
if not url:
raise ValueError(f"Unknown dashboard '{name}'")
return url
def _get_available_vpn_configs() -> list:
try:
available = vpn_runtime.list_available_vpns()
except Exception:
available = []
configured = getattr(core_functions, "VPN_CONFIG_NAMES", [])
if not available:
return configured
ordered = [name for name in configured if name in available]
extras = [name for name in available if name not in ordered]
return ordered + extras
logging.basicConfig(level=logging.DEBUG)
app = Flask(__name__)
app.secret_key = os.getenv("FLASK_SECRET_KEY", "dev-secret")
@app.context_processor
def inject_global_options():
return {
"dashboard_names": list(DASHBOARD_URLS.keys()),
"available_vpn_configs": _get_available_vpn_configs(),
}
def _format_ipv6(host: str) -> str:
return f"[{host}]" if ":" in host and not host.startswith("[") else host
@@ -393,6 +434,23 @@ def hnk_page():
def network_clients_page():
return render_template("pages/network_clients.html", active_page='network_clients')
@app.post("/api/supis/list")
def api_list_supis():
data = request.get_json(silent=True) or {}
host = (data.get("host") or "").strip()
try:
limit = int(data.get("limit", 500))
except (TypeError, ValueError):
return jsonify({"error": "limit must be an integer"}), 400
if not host:
return jsonify({"error": "Host IP is missing"}), 400
try:
supis = core_functions.list_network_clients(host, limit=limit)
return jsonify({"count": len(supis), "supis": supis})
except Exception as e:
app.logger.error(f"Error listing SUPIs for host {host}: {e}", exc_info=True)
return jsonify({"error": str(e)}), 502
@app.route("/system-browser")
def system_browser_page():
return render_template("pages/system_browser.html", active_page='system_browser')
@@ -457,7 +515,10 @@ def gaf_desk_page():
def api_list_m2000():
data = request.json
dashboard_name = data.get('dashboard')
base_url = DASHBOARD_URLS.get(dashboard_name)
try:
base_url = _resolve_dashboard_url(dashboard_name)
except ValueError as exc:
return jsonify({"error": str(exc)}), 400
try:
token, session = auth_utils.get_vpn_dashboard_token(base_url)
devices = core_functions.list_m2000_vpns(base_url, token, session)
@@ -470,7 +531,10 @@ def api_list_m2000():
def api_get_network_config():
data = request.json
dashboard_name = data.get('dashboard')
base_url = DASHBOARD_URLS.get(dashboard_name)
try:
base_url = _resolve_dashboard_url(dashboard_name)
except ValueError as exc:
return jsonify({"error": str(exc)}), 400
try:
token, session = auth_utils.get_vpn_dashboard_token(base_url)
config_data = core_functions.get_full_network_config(base_url, token, session)
@@ -483,7 +547,10 @@ def api_get_network_config():
def api_list_tenants():
data = request.json
dashboard_name = data.get('dashboard')
base_url = DASHBOARD_URLS.get(dashboard_name)
try:
base_url = _resolve_dashboard_url(dashboard_name)
except ValueError as exc:
return jsonify({"error": str(exc)}), 400
try:
token, session = auth_utils.get_vpn_dashboard_token(base_url)
tenants = core_functions.list_tenants(base_url, token, session)
@@ -497,7 +564,10 @@ def api_list_plmns():
data = request.json
dashboard_name = data.get('dashboard')
tenant_id = data.get('tenant_id')
base_url = DASHBOARD_URLS.get(dashboard_name)
try:
base_url = _resolve_dashboard_url(dashboard_name)
except ValueError as exc:
return jsonify({"error": str(exc)}), 400
try:
token, session = auth_utils.get_vpn_dashboard_token(base_url)
plmns = core_functions.list_plmns(base_url, token, session, tenant_id)
@@ -563,7 +633,10 @@ def api_list_plmn_hnks():
dashboard_name = data.get('dashboard')
tenant_id = data.get('tenant_id')
plmn_id = data.get('plmn_id')
base_url = DASHBOARD_URLS.get(dashboard_name)
try:
base_url = _resolve_dashboard_url(dashboard_name)
except ValueError as exc:
return jsonify({"error": str(exc)}), 400
try:
token, session = auth_utils.get_vpn_dashboard_token(base_url)
hnks = core_functions.list_plmn_hnks(base_url, token, session, tenant_id, plmn_id)
@@ -675,7 +748,10 @@ def api_update_radios():
network_id = data.get('network_id')
new_count = data.get('new_count')
operation = data.get('operation')
base_url = DASHBOARD_URLS.get(dashboard_name)
try:
base_url = _resolve_dashboard_url(dashboard_name)
except ValueError as exc:
return jsonify({"error": str(exc)}), 400
try:
token, session = auth_utils.get_vpn_dashboard_token(base_url)
result = core_functions.update_radio_count(base_url, token, session, network_id, new_count, operation)
@@ -693,6 +769,66 @@ def api_get_system_browser_data():
app.logger.error(f"Error getting system browser data: {e}", exc_info=True)
return jsonify({"error": str(e)}), 500
@app.post("/api/logs/processes")
def api_logs_processes():
data = request.get_json(force=True) or {}
hosts = data.get("hosts") or []
if not isinstance(hosts, list) or not hosts:
return jsonify({"error": "Provide a list of host IPs"}), 400
results = []
for raw_host in hosts:
host = (raw_host or "").strip()
if not host:
continue
try:
token = auth_utils.authenticate(host)
system_info = core_functions.get_system_info(host, token)
frontend = core_functions.get_frontend_config(host, token)
services = frontend.get("services", []) if isinstance(frontend, dict) else []
running = [svc for svc in services if svc.get("state") == "started"]
results.append({
"host": host,
"hostname": system_info.get("hostname"),
"services": running,
})
except Exception as exc:
app.logger.error(f"Failed to fetch services for {host}: {exc}", exc_info=True)
results.append({"host": host, "error": str(exc)})
return jsonify({"hosts": results})
@app.post("/api/logs/stream")
def api_logs_stream():
data = request.get_json(force=True) or {}
targets_in = data.get("targets") or []
if not isinstance(targets_in, list) or not targets_in:
return jsonify({"error": "No log targets supplied"}), 400
targets: list[LogTarget] = []
for item in targets_in:
host = (item.get("host") or "").strip()
processes = [p.strip() for p in item.get("processes", []) if p and p.strip()]
if host and processes:
targets.append(LogTarget(host=host, processes=processes, hostname=item.get("hostname")))
if not targets:
return jsonify({"error": "No valid hosts/processes to stream"}), 400
try:
streamer = JournalctlStream(targets)
except Exception as exc:
return jsonify({"error": str(exc)}), 500
def generate():
try:
for event in streamer.iter_events():
yield json.dumps(event) + "\n"
finally:
streamer.stop()
return Response(stream_with_context(generate()), mimetype="text/plain")
@app.route("/api/backup/create", methods=["POST"])
def api_create_backup():
data = request.json
@@ -741,7 +877,10 @@ def api_get_host_details():
def api_list_users():
data = request.json
dashboard_name = data.get('dashboard')
base_url = DASHBOARD_URLS.get(dashboard_name)
try:
base_url = _resolve_dashboard_url(dashboard_name)
except ValueError as exc:
return jsonify({"error": str(exc)}), 400
try:
token, session = auth_utils.get_vpn_dashboard_token(base_url)
users = core_functions.list_users(base_url, token, session)
+11 -3
View File
@@ -1,9 +1,16 @@
import os
import requests
import json
from requests.exceptions import HTTPError
requests.packages.urllib3.disable_warnings()
DASHBOARD_USER = os.getenv("DASHBOARD_USER", "admin@hpe.com")
DASHBOARD_PASSWORD = os.getenv(
"DASHBOARD_PASSWORD",
"kxly7o6FboYUoQXSeLrs2xodHIeybQRwcMs33QC5#",
)
def _format_ipv6(host_ip):
"""If the host_ip is an IPv6 address, enclose it in square brackets."""
if ":" in host_ip and not host_ip.startswith("["):
@@ -35,9 +42,10 @@ def get_vpn_dashboard_token(base_url):
})
credentials = {
"user": "admin@hpe.com", "password": "JohnWayne#21",
# "user": "admin@athonet.com", "password": "administratoR!1",
"lang": "en", "auth_provider": "enterprise"
"user": DASHBOARD_USER,
"password": DASHBOARD_PASSWORD,
"lang": "en",
"auth_provider": "enterprise",
}
auth_response = session.post(f"{base_url}/portal/api/session/authenticate", json=credentials, verify=False)
+70 -30
View File
@@ -5,20 +5,32 @@ import subprocess
import time
import re
import os
from typing import Any, Dict, List
import auth_utils
import hashlib
from requests.exceptions import HTTPError
from datetime import datetime
from services import vpn_runtime
from services.vpn_runtime import VPNRuntimeError
requests.packages.urllib3.disable_warnings()
VPN_CONFIG_NAMES = ["Triton", "Star", "Bluebonnet", "Lonestar", "Production", "US-Support", "EU-Support"]
_VPN_NAMES_ENV = os.getenv(
"VPN_CONFIG_NAMES",
"Triton,Star,Bluebonnet,Lonestar,Production,US-Support,EU-Support",
)
VPN_CONFIG_NAMES = [name.strip() for name in _VPN_NAMES_ENV.split(",") if name.strip()]
def _format_host_for_https(host: str) -> str:
return f"[{host}]" if ":" in host and not host.startswith("[") else host
SERIAL_PASSWORDS = {
"3M1D2211Z3": "EP5G!f15878b4af20", "3M1D10146B": "EP5G!076689528baf",
"3M1D10146G": "EP5G!c3b0072cabf5", "3M1D2211Z1": "EP5G!65b22ae8617a",
"3M1D19125H": "EP5G!da3c04fde559", "3M1D19125G": "EP5G!b73f98633108",
"3M1D19125F": "EP5G!e61201fb9234", "3M1D1R16M4": "EP5G!ca439b544329"
"3M1D19125F": "EP5G!e61201fb9234", "3M1D1R16M4": "EP5G!ca439b544329",
"3M1D10146F": "EP5G!d1343c34875d"
}
def list_home_network_keys(host_ip, token):
@@ -206,38 +218,25 @@ def set_vpn_endpoint(host_ip, region):
return {"status": "success", "message": f"VPN endpoint switched to {region} ({new_ip})."}
def get_active_vpn():
for name in VPN_CONFIG_NAMES:
for pattern in ["openvpn-client@{name}.service", "openvpn@{name}.service"]:
try:
service_name = pattern.format(name=name)
cmd = ["/usr/bin/sudo", "/usr/bin/systemctl", "is-active", service_name]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.stdout.strip() == "active":
return name
except FileNotFoundError:
continue
return None
return vpn_runtime.get_active_vpn()
def toggle_vpn_connection(vpn_name, turn_on):
active_vpn = get_active_vpn()
if active_vpn:
subprocess.run(["/usr/bin/sudo", "/usr/bin/systemctl", "stop", f"openvpn-client@{active_vpn}.service"])
subprocess.run(["/usr/bin/sudo", "/usr/bin/systemctl", "stop", f"openvpn@{active_vpn}.service"])
vpn_name = (vpn_name or "").strip()
if turn_on and vpn_name and VPN_CONFIG_NAMES and vpn_name not in VPN_CONFIG_NAMES:
raise ValueError(f"Unsupported VPN '{vpn_name}'")
if turn_on:
try:
service_name = f"openvpn-client@{vpn_name}.service"
cmd = ["/usr/bin/sudo", "/usr/bin/systemctl", "start", service_name]
subprocess.run(cmd, check=True)
except subprocess.CalledProcessError:
service_name = f"openvpn@{vpn_name}.service"
cmd = ["/usr/bin/sudo", "/usr/bin/systemctl", "start", service_name]
subprocess.run(cmd, check=True)
time.sleep(4)
if get_active_vpn() != vpn_name:
raise Exception(f"Connection failed for {vpn_name}. Check OpenVPN logs for details.")
try:
if turn_on:
if not vpn_name:
raise ValueError("vpn_name is required when turning on a VPN")
vpn_runtime.start_vpn(vpn_name)
else:
vpn_runtime.stop_active_vpn()
except VPNRuntimeError as exc:
raise Exception(str(exc)) from exc
return get_active_vpn()
return vpn_runtime.get_active_vpn()
def get_full_network_config(base_url, token, session):
network_url = f"{base_url}/portal/api/1/network"
@@ -371,6 +370,47 @@ def get_system_browser_data():
return list(vpn_clients.values())
def _extract_subscriber_list(payload: Any) -> List[Dict[str, Any]]:
if isinstance(payload, list):
return payload
if not isinstance(payload, dict):
return []
for key in ("subscribers", "items", "data", "results"):
value = payload.get(key)
if isinstance(value, list):
return value
return []
def list_network_clients(host_ip: str, limit: int = 500, page_size: int = 200, timeout: float = 15.0) -> List[Dict[str, Any]]:
"""Fetch SUPI/network client data from the target ComboCore host."""
if limit <= 0:
raise ValueError("limit must be positive")
token = auth_utils.authenticate(host_ip)
formatted_host = _format_host_for_https(host_ip)
base_url = f"https://{formatted_host}/core/udm/api/1/status/supis/"
headers = {"Authorization": f"Bearer {token}"}
collected: List[Dict[str, Any]] = []
offset = 0
while len(collected) < limit:
batch_size = min(page_size, limit - len(collected))
params = {"limit": batch_size, "offset": offset}
resp = requests.get(base_url, headers=headers, params=params, verify=False, timeout=timeout)
resp.raise_for_status()
data = resp.json()
items = _extract_subscriber_list(data)
if not isinstance(items, list):
raise RuntimeError("Unexpected subscriber payload format")
collected.extend(items)
if len(items) < batch_size:
break
offset = data.get("next_offset", offset + batch_size) if isinstance(data, dict) else offset + batch_size
return collected[:limit]
# ------- System ID Data Begin ---------
def _make_host_api_get_request(host_ip, token, endpoint):
+30
View File
@@ -0,0 +1,30 @@
version: "3.8"
services:
app:
build: .
image: network-tool:latest
ports:
- "5050:8000"
env_file:
- .env
volumes:
- ./ansible_workspace:/app/ansible_workspace
- ./openvpn/configs:/vpn/configs
- ./openvpn/runtime:/vpn/runtime
#- openvpn-state:/vpn/runtime
- ./keys/5G-SSH-Key.pem:/app/.ssh/5G-SSH-Key.pem:ro
- ./keys/5G-SSH-Key.pem:/root/.ssh/5G-SSH-Key.pem:ro
devices:
- /dev/net/tun:/dev/net/tun
cap_add:
- NET_ADMIN
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://127.0.0.1:8000/api/ping"]
interval: 30s
timeout: 5s
retries: 3
volumes:
openvpn-state:
+38
View File
@@ -0,0 +1,38 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
NhAAAAAwEAAQAAAYEAv81946SvXssgs4GbLu4PZx9cHGuyU9P/9MonOhWceUYJ9QVLnm1/
E13FOdVKfIy8T5lyb2EUReKZKLzsEMIjr+BTKNlFNLplyyTOC5CYTlPJIFCxCV5G+Kv+Tt
KrAJdm3VgMuA0BSs3O9vg4Y35J+/CIG47MOVAM4KfX2EVYhH5eNBuI13YiBhv7n/KRPAx5
PKkA9PHjmihB2ua+tyDpkkOuU54v0v6uTOsfDjA5Qs22tb1GMix1P5pNB0ovgzjXB6b2xq
HP7fvECcMNJSBky+kQfiK2tDUxoCssJDqSZccC7l2qknK9nFlJtZfL+Nb9ZyyOnXaMWhJA
FV6HKrZr5EHKWNEzkdO5JNGn0HbcIZsvbYTc+h2bRKBEeNWvkFihmHen962s/MDhrdRXLl
VwdhBtdtBIgBbp+oC/E2M8Sf6Gl03/QNkTgXFlUzEzodv7PysnKIz8PoGDXp/jk1OZqxk5
+5wPhVGT9DI9Y7XLVGzU7BAQkf2ncCDv16lXiPc7AAAFiLCDAfiwgwH4AAAAB3NzaC1yc2
EAAAGBAL/NfeOkr17LILOBmy7uD2cfXBxrslPT//TKJzoVnHlGCfUFS55tfxNdxTnVSnyM
vE+Zcm9hFEXimSi87BDCI6/gUyjZRTS6ZcskzguQmE5TySBQsQleRvir/k7SqwCXZt1YDL
gNAUrNzvb4OGN+SfvwiBuOzDlQDOCn19hFWIR+XjQbiNd2IgYb+5/ykTwMeTypAPTx45oo
Qdrmvrcg6ZJDrlOeL9L+rkzrHw4wOULNtrW9RjIsdT+aTQdKL4M41wem9sahz+37xAnDDS
UgZMvpEH4itrQ1MaArLCQ6kmXHAu5dqpJyvZxZSbWXy/jW/Wcsjp12jFoSQBVehyq2a+RB
yljRM5HTuSTRp9B23CGbL22E3Podm0SgRHjVr5BYoZh3p/etrPzA4a3UVy5VcHYQbXbQSI
AW6fqAvxNjPEn+hpdN/0DZE4FxZVMxM6Hb+z8rJyiM/D6Bg16f45NTmasZOfucD4VRk/Qy
PWO1y1Rs1OwQEJH9p3Ag79epV4j3OwAAAAMBAAEAAAGBAJIvbsm4VSlsrEnkeIB2VSsFzd
CjjNEzfZv3D5rHqfEMnr4vAQmI7xe1moKPvCvdoBETJRMa7LavFIjmJQ5IyaZc1UUHBCZC
Ax+nt5s847ifR2Xn2mcHghQ6EqPFESxsOKxvVZJZ5yg6YIn/egrq0DzDgRlv5tuv1YDMrE
hb4jFplycj7VI66Ye6gDfSSzt3Tlgbf20xh4WRNVBAGg+9eAhQqWd144FvNy3M7miD/MCb
xawSzJOlKuAFHLx3oGrlQDmOFMPcLN2E7mXqWS5XZtQ4bBkF2nUVMs/mLjHMdFdLvR9u5t
yLHEedpc7PXJ/66P5o+ebDZL5tG9B0/fXNsc73W+mdV/FrwBnpn3PbPj9RpIs+b6v1MwQI
1i6y3z3SfYTEFNkMwtYsC75i3ga4N7v4I/CmlGczYwoq6zY2ZSg03fQ0AzyDQxgzIkSc6w
pGuPEvp3Er/NolHd1ErSDKWlGzsICCif9ES6W6LvDqoi9TyuqIiaAnRx8IE/EA9COaYQAA
AMEAl2Tv8te3LvnLzuYaobqltjXk3PFEG2GvP5wnr87ItSX99Iv4avycvKA67xk80hC6Pq
MaL7AyRkkP8vs3lMCRx90Kwy+ZZZonPuvClwFd8yNhk74ZzbhgPgd6oVM8eI8mmWZWcCzz
6kp6AQ3P1x5ZI0UB3oQHYGDzzFICaaj35I2XpBkQN29ErtRV5IQRN+ZKi6yZQnWaxJDPi6
K2HEyPnS2Te+Ck6q6t3Os+YWMlgr/s1eFRWCYLioWHNGl5krHsAAAAwQD/kFZgz+/6b7v0
FbaUW016K4xj1FpgAI6YF3w6fy1g+2pmFaFx4HAWdWiob0oPAhRoYBPrPPPKACYXtXgNBm
IkBJK/7uTdvOxJqrXlM2rDK+BgyZLo/y8U1AARgEw0x1oFnJLNsT/qYhfeHScizp/Lj5qF
JznZMu8Hbhdi3XnPmPMH1AuaQ2E6Leh+5Wl0svw3/dXHc8dRh7AFyUMDN8S2ujWKGiRUOl
NQBmaSJIBOj4pKn4ixbK1GaSLa8i4cqYMAAADBAMAhS6DjH0yp6Vb/hN9i5d/ASC7BbsEL
Zo9IafnFNBCXyfaazg7Rdez/b0qx2n07j7gXuYq0IUVzG9FQTMxC7Rkc8eP4jYC6BHSEjb
R46FnVP4v7wjqBQCKTaw9J6xvC1SoHTeISgXRozaHssR5IiEHeiuFzuuNTvM30+TbYAsuc
ASFRLdv0i0IoZ3A03zlrsDbeTkTsZxw4LYQ5lMHi99C9wmZmsQMob/9RPSbHshwgycXnCx
jthSo+rcUFcoZl6QAAABB1YnVudHVAeW9jdG8tc2RrAQ==
-----END OPENSSH PRIVATE KEY-----
+157
View File
@@ -0,0 +1,157 @@
client
remote vpn.us-east-2.p5g.athonet.cloud 1091
comp-lzo yes
dev tun
proto udp
askpass /vpn/configs/cm-prod.auth
nobind
script-security 2
persist-key
persist-tun
<cert>
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
74:c7:cc:25:45:57:b9:0e:4c:f3:e8:dd:a4:5c:29:57
Signature Algorithm: sha256WithRSAEncryption
Issuer: CN=ras-server
Validity
Not Before: Feb 13 13:45:21 2026 GMT
Not After : May 18 13:45:21 2028 GMT
Subject: CN=client
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
Modulus:
00:8e:54:19:52:6b:8c:7a:bf:1b:5a:eb:88:a1:06:
ae:3a:17:b3:bb:9d:e9:44:ab:68:5d:2c:12:51:ea:
c2:df:53:d7:19:61:9d:8f:cb:f0:0f:ba:70:d9:d3:
7a:c4:42:4d:27:fe:fb:ba:fc:f0:37:fa:7e:7e:26:
86:b2:dd:81:b8:b0:db:5c:e1:13:0a:4f:87:72:df:
46:0b:62:e0:11:19:f4:66:35:a4:76:28:1c:8c:9d:
46:13:10:22:d4:45:8f:7f:45:6c:38:a1:28:49:07:
38:8f:14:81:05:d0:25:4c:83:b3:1a:ec:91:ce:06:
68:64:04:fb:b0:6e:84:46:58:34:f9:1c:83:26:ba:
9a:f4:e9:62:47:5d:3d:3b:05:56:2c:0a:ff:0a:a5:
bb:7a:c5:34:23:84:ea:2b:16:ad:72:f7:22:3a:88:
df:25:78:03:58:a5:bf:a2:be:73:c9:d0:ce:46:3d:
04:27:fc:8c:ca:02:23:70:c9:83:be:c0:50:97:6b:
e1:cc:3e:77:5d:b5:8a:ce:8c:27:c0:40:3e:db:ea:
b4:29:f0:79:5a:0a:c0:f5:62:d7:6e:71:31:71:5a:
85:6a:20:66:86:16:a5:82:c3:89:7d:b5:3e:3a:9f:
89:75:1f:1f:cd:1b:aa:2f:d6:f2:4b:58:79:1f:21:
63:a1
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Basic Constraints:
CA:FALSE
X509v3 Subject Key Identifier:
51:BE:CE:53:81:1A:FE:DA:8E:BA:87:A9:09:CA:E8:D3:4D:02:1A:DA
X509v3 Authority Key Identifier:
keyid:17:2C:34:81:52:BB:79:9E:59:F6:AF:8E:18:C0:A9:14:4E:30:86:B0
DirName:/CN=ras-server
serial:71:BA:B6:63:39:2D:E7:71:DC:77:10:BB:43:3E:4B:47:AD:EF:5D:80
X509v3 Extended Key Usage:
TLS Web Client Authentication
X509v3 Key Usage:
Digital Signature
Signature Algorithm: sha256WithRSAEncryption
Signature Value:
28:f2:80:23:d0:51:4b:07:b2:99:62:31:8c:3f:ca:ba:30:2c:
3c:84:da:96:81:f0:18:78:e2:33:8a:cc:12:05:03:9a:e4:ca:
7e:06:73:f2:27:c3:c9:49:94:6b:91:71:57:a7:40:45:59:f2:
39:b3:98:ce:8e:7a:c0:ac:eb:72:4f:7c:e4:c9:47:a0:01:99:
7a:74:9a:29:f0:b3:1b:e5:24:20:c2:f4:77:fd:d9:41:c5:3b:
5e:ba:df:d4:ec:dc:5b:45:f2:e8:95:4b:5e:8f:e1:b3:ce:1a:
b9:82:a3:03:52:cc:d3:7d:76:e7:0e:e9:39:e3:93:87:92:66:
7a:f0:b3:ca:39:7c:06:2b:96:df:fd:87:a8:81:1c:ba:ee:c5:
92:0b:0c:0f:e9:8f:87:f0:49:ed:8e:e7:2b:e5:7a:19:05:01:
88:14:df:2c:5d:8e:b7:1e:66:01:f0:c5:b2:be:9d:e6:78:f8:
60:15:a0:b4:8c:46:e6:35:e6:d0:c6:3b:55:d6:1c:f5:40:90:
df:5f:bf:9c:dc:14:b6:aa:66:1d:98:ca:00:54:b7:09:59:79:
fe:ad:f9:70:26:fe:cf:a9:83:dd:7e:29:c9:8a:51:8b:42:96:
76:2e:42:2a:ee:0e:1e:cc:a6:e8:87:fa:40:be:58:54:40:c8:
d8:55:53:02
-----BEGIN CERTIFICATE-----
MIIDUjCCAjqgAwIBAgIQdMfMJUVXuQ5M8+jdpFwpVzANBgkqhkiG9w0BAQsFADAV
MRMwEQYDVQQDDApyYXMtc2VydmVyMB4XDTI2MDIxMzEzNDUyMVoXDTI4MDUxODEz
NDUyMVowETEPMA0GA1UEAwwGY2xpZW50MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
MIIBCgKCAQEAjlQZUmuMer8bWuuIoQauOhezu53pRKtoXSwSUerC31PXGWGdj8vw
D7pw2dN6xEJNJ/77uvzwN/p+fiaGst2BuLDbXOETCk+Hct9GC2LgERn0ZjWkdigc
jJ1GExAi1EWPf0VsOKEoSQc4jxSBBdAlTIOzGuyRzgZoZAT7sG6ERlg0+RyDJrqa
9OliR109OwVWLAr/CqW7esU0I4TqKxatcvciOojfJXgDWKW/or5zydDORj0EJ/yM
ygIjcMmDvsBQl2vhzD53XbWKzownwEA+2+q0KfB5WgrA9WLXbnExcVqFaiBmhhal
gsOJfbU+Op+JdR8fzRuqL9byS1h5HyFjoQIDAQABo4GhMIGeMAkGA1UdEwQCMAAw
HQYDVR0OBBYEFFG+zlOBGv7ajrqHqQnK6NNNAhraMFAGA1UdIwRJMEeAFBcsNIFS
u3meWfavjhjAqRROMIawoRmkFzAVMRMwEQYDVQQDDApyYXMtc2VydmVyghRxurZj
OS3ncdx3ELtDPktHre9dgDATBgNVHSUEDDAKBggrBgEFBQcDAjALBgNVHQ8EBAMC
B4AwDQYJKoZIhvcNAQELBQADggEBACjygCPQUUsHspliMYw/yrowLDyE2paB8Bh4
4jOKzBIFA5rkyn4Gc/Inw8lJlGuRcVenQEVZ8jmzmM6OesCs63JPfOTJR6ABmXp0
minwsxvlJCDC9Hf92UHFO16639Ts3FtF8uiVS16P4bPOGrmCowNSzNN9ducO6Tnj
k4eSZnrws8o5fAYrlt/9h6iBHLruxZILDA/pj4fwSe2O5yvlehkFAYgU3yxdjrce
ZgHwxbK+neZ4+GAVoLSMRuY15tDGO1XWHPVAkN9fv5zcFLaqZh2YygBUtwlZef6t
+XAm/s+pg91+KcmKUYtClnYuQiruDh7MpuiH+kC+WFRAyNhVUwI=
-----END CERTIFICATE-----
</cert>
<key>
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIFHDBOBgkqhkiG9w0BBQ0wQTApBgkqhkiG9w0BBQwwHAQIa+OAPnbBLPoCAggA
MAwGCCqGSIb3DQIJBQAwFAYIKoZIhvcNAwcECMIGNyqOMgmdBIIEyCKHB6hSeetF
GmqrqPawa4TZt5sRqFJOOh2ctVr7e/vs7pfbxpyZ3uFauDktOtnZYL2HbDfuBegE
1T5tOOXSoB+8K2ITggMNTx8Qdqutdrzpb/D0H9FOp4b9ey3e3BB0xUphVRbUA9VE
spJGvTKiox6N1JE6PXAtU/1LvjLaZpI0kWAu+dokPDpVcLYrUPgG4wYMtrtSAgS6
2ViU7Emhnx4JzVCHbPw8ssnDMQe9A43+fMlthBYHYMZRLZvjpGmE825U0TJD8g6S
JFbFXL+9ptphO7SAhgPy7jmFyVJ/Ydp22JRj6fijLkYxncL/RA4rB8uakmaEzKev
47CAAMJC0RmKmesggmaSprvLYlSW/ZUoK2PztrNnQLwe9T5/UiXe9e61XbxrYpYY
xVXuxEjMUXhaHvoLi8vcuPD1Oir52b5sNyis2L5W5/iBnyUPZTPf7gKYYndP/wAg
DgEablWGLrRGmdd/zAzU5Vb5csIHqoHC0kAlHAK7Yz7Dn1jwUI1YoLA3RUxht6ZL
T5HohPgEyXZDrZExBvKyVu+qxHE4emy1kkHMqV83y/9xQbTy44akwdJ0z7LRweOD
ja7exF7D4MX0BLLUTotGv7p9fe34qeJC5kSNT3gFEgxPiznFslmMMHAvt/siJcy3
3hxbNUS1xUq0jxXcPjTioiox2mcplIAHyILlOXhguwFPlVBNv1R/QjndrZ1zJsnH
zJ9CC/kkPEVp0BSZ2y0kliSh0U0a00gFc9w7hxXT9wv60wZ427koTLZbdfSHYELg
WvWgtU7OBEYa0HDGiLb2O4kXbNn0JsoYGmE18HmNF3a4lXrYjNZcNHsPw2aduVPz
EdBv4QfKn0hFjgFY/qh90LqQ6Qa83jhSgPQaoyzODMjDcy0dTNAexgNg7eKL6VLW
T76zJ3i77jJF2yuUlDqFWatEtjAbBtgoRoHkSqWdG+Nd+/O7vve1bptZWTeaQSTu
jl4VHLviWZzJtMA8iSvskiSWc0YmoxvxbakUMTeztvopqkrA+x8J+um0gwD6lnjV
T6Tway5MKnbEuHA1dFFxNIPDxwDrzNcPBuUVAV8biP0M6f1tAFn9mGUViiRxSv3n
8nTjgzl8KE7IMig9kLoI5iSmL2jKgB/WB6mWsL+UF/stJ5Q8axNjcBVrd2T1C6SH
ZN8HWgtnu38sA19nCTpnKxkDZD0hJTvF/Y/8J2HCkJV9NBXZcMSHUJqZikmBLxcq
bxtbTG9peM6FqtCciUaCIs6spH7s8Bz0HANwfGG50OPN/8TIhFPRlzMNfRwbmKbY
E6EOywYKRE2BsETYRbfs4dZP8tVuqI5jdOn2BDnip4swZOoRcboEzE8HFgjnAJPg
cK14ii9S/mQxDDOsRWaZmSK0T9MJuSJpKTZWkbbjFhmTEq9Q89oqv5/bMZS9utga
Zxud353mqKLQJbwqJFouggQKY3QjjAdx3dUg42rhbSzxOuypgXDCg7Ydrxe0BeFf
sB1h71wmnJkjzZXbBRqjlExmPfasVwar52YUclOqjYBEBUKdErUVoI0GtcKo/Ike
3emQhIv0eJXkDSaU72fTHTiCY6cwfm/HwPZ1IJ+UW+CyhXO2gQX5YoYEqkxCFMxF
N0hRJ8XbnEQGvsfQSsnm1Q==
-----END ENCRYPTED PRIVATE KEY-----
</key>
<ca>
-----BEGIN CERTIFICATE-----
MIIDSzCCAjOgAwIBAgIUcbq2Yzkt53HcdxC7Qz5LR63vXYAwDQYJKoZIhvcNAQEL
BQAwFTETMBEGA1UEAwwKcmFzLXNlcnZlcjAeFw0yNjAyMTMxMzQxMTdaFw0zNjAy
MTExMzQxMTdaMBUxEzARBgNVBAMMCnJhcy1zZXJ2ZXIwggEiMA0GCSqGSIb3DQEB
AQUAA4IBDwAwggEKAoIBAQCXbky7LHJ/AdngYmP/NsDkjqr0q1ChksXSn9nfY0+w
7QFrD+OW0lcLYj3nOBbgtfsJo7YEPC2flws7hath/F0QrcBXBRRtYPjYgNuyWjUL
34uNTb0MdQjcVD7n7nBbmFYaYcmS+6BAB1nE8Y8wkQhcuMSKxWaE92ivhEQlCxSZ
NuAEmgfDK7Ow4Vq/MhvFTD+lxpctaJVeslU38De5kuYWd04jOOphitrRvdSHm0fE
zRI2lWfNp5a6f6QV6uVdSOK5CacK6Fv8mKOn3UW3qr1AJyhJHbg1hu1ZS8+KtAq0
bhnY9vQyfP3xhLe8Hp019uqW1WG5P7mberIAsXWXEYX3AgMBAAGjgZIwgY8wDwYD
VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUFyw0gVK7eZ5Z9q+OGMCpFE4whrAwUAYD
VR0jBEkwR4AUFyw0gVK7eZ5Z9q+OGMCpFE4whrChGaQXMBUxEzARBgNVBAMMCnJh
cy1zZXJ2ZXKCFHG6tmM5Ledx3HcQu0M+S0et712AMAsGA1UdDwQEAwIBBjANBgkq
hkiG9w0BAQsFAAOCAQEAFv8JCCYmzgL+g6QEh+cCmwL/oxLsZR19cqoATIvx1A83
uOVa3HeNKe6iaLr+Qo3qk9SuGfJeNL/ETiW+UlQkQN6WptE/5J36XfcvsgEUes0r
8kCZybO/m9xTKDm/kZ3fRy6w5E79uT674Cxkzi+03bIV40X/IPO7YgIlwqOEoGBt
cWlGWKFNIJmdRUDqUQAtrZ0bFQyTefesvZ8BTM5grOGvEPtjHz4VRPOE/114Msf1
u0NudAeHo9qqTxH4SzX6AEYvkIrbK0Cv6Z3aeu0coSnXyWGgCBdVAuzH7rbTo5ex
s97EKGeaY5pQbntmnuB5x4bhryRFVRfY41ngfL9PkA==
-----END CERTIFICATE-----
</ca>
+115
View File
@@ -0,0 +1,115 @@
client
remote 128.136.82.165 1499
auth-user-pass /vpn/configs/noc-us.auth
comp-lzo yes
dev tun
proto udp
key-direction 1
nobind
;auth-nocache
script-security 2
persist-key
persist-tun
data-ciphers AES-256-GCM:AES-128-GCM:AES-256-CBC:AES-128-CBC
data-ciphers-fallback AES-256-CBC
allow-compression yes
<tls-auth>
#
# 2048 bit OpenVPN static key
#
-----BEGIN OpenVPN Static key V1-----
a4a76c0cfe4f90c3e69899712226a4cb
5fc51962c16455a069a3a17812e5b876
7f7761e00ab20fd34448dc1341fcd11d
5343474e49665b613edecd9eb31be1fa
29b4d0c22ea0fa0fb48ad6fe6bf78905
87ecd070d735908a480dfa1a393d8fef
74ee18dea11c0be9fe800b6b50859ce9
b608b7f717ded5c6708bf3e6b40d9af5
c8247886749596ab5c82aee7fc863a12
3fc47c54e1a310001636943e20981992
0e316b6e697ef2298060c8176160a69b
1ef2e68145c46cc92664ab2dc5349eeb
1598deb0145a2c76318f03689436a426
3dd84e22912900d63f4869524cdba5e8
e8dc2d46b89e8cd6418f36ffaed3f561
68cb4cf047aa2433793291908b3ba334
-----END OpenVPN Static key V1-----
</tls-auth>
<cert>
-----BEGIN CERTIFICATE-----
MIIDRTCCAi2gAwIBAgIRAJ6L07yCYjna3T2iqXTNXGYwDQYJKoZIhvcNAQELBQAw
DTELMAkGA1UEAwwCQ0EwIBcNMjExMjI3MTIyODU4WhgPMjA5MDAxMTQxMjI4NTha
MBExDzANBgNVBAMMBnJlbW90ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
ggEBAOAoz5bO6nK0r9QMkOVzV1f464DGtZKWcXGT280qaORUHH5TGTTseVH0a36b
6qVvCBBRPWuettClLerltQHe5OoIJGkkKZ5ZVQyDzfjNcLqp+ieZVI5EognWuCUx
w7HgAfRbFrFKSAyX1xtSrOypoKwrM3XfaFOFCye9T3Vt5QjcWm5PfMKJqs3Ljmeb
op8XoRIM0guxXlis6a+m8koWDBQArOUHtTjmfJWFOZiBU++lk3hQerUf9vOJWybU
fzraB2BBz4k/qbOzNxv4NNOP7FEIc9ijfN11DUADZqzjPffADTa3Ipd5mN9Otvue
cgHq2iKrERKJq0gqJH4BdYD3PP0CAwEAAaOBmTCBljAJBgNVHRMEAjAAMB0GA1Ud
DgQWBBSef+DbTCUUcYbONWB4dm0eDh56sDBIBgNVHSMEQTA/gBTs/L0I4+lJVT31
mBoXOslN+NydgKERpA8wDTELMAkGA1UEAwwCQ0GCFDSh+PclVE+hqkJaKiJBxpSh
bKtbMBMGA1UdJQQMMAoGCCsGAQUFBwMCMAsGA1UdDwQEAwIHgDANBgkqhkiG9w0B
AQsFAAOCAQEACPVwrl1hrERJDsaS1L4PXqTIwGj8G9oD9cx+bbZFa8YYYzot9742
5oeEjZEttCFbt6r5EY2/YG2/hC78L04QF6CpuIDVuJrcFqk6lb6SIXCWyZxl7x7I
lHHdommSxhuiKvnBdV1VELsuxGwUADObuFuhS60V/GlrOHaCbdJyxEzJgSVKmBaw
YHEt91ficzynYMci5q23/L9DF4WSkrRJJYIOraCpjiQ5Bg0GyJNrqMVdKfeS38XO
W7ZpCcy8/jb9noHCVAyXewhNpUtue2dEZF4axQIDHpPky/UoSpk7sUnKG6bYokG+
9IRT6CA/KnfUtC36oyHEZWjmfTB0pHMZjA==
-----END CERTIFICATE-----
</cert>
<key>
-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDgKM+WzupytK/U
DJDlc1dX+OuAxrWSlnFxk9vNKmjkVBx+Uxk07HlR9Gt+m+qlbwgQUT1rnrbQpS3q
5bUB3uTqCCRpJCmeWVUMg834zXC6qfonmVSORKIJ1rglMcOx4AH0WxaxSkgMl9cb
UqzsqaCsKzN132hThQsnvU91beUI3FpuT3zCiarNy45nm6KfF6ESDNILsV5YrOmv
pvJKFgwUAKzlB7U45nyVhTmYgVPvpZN4UHq1H/bziVsm1H862gdgQc+JP6mzszcb
+DTTj+xRCHPYo3zddQ1AA2as4z33wA02tyKXeZjfTrb7nnIB6toiqxESiatIKiR+
AXWA9zz9AgMBAAECggEBAK70S2nKX8RdcGqR0Dm193MLLkxZS3h5AVwDamfMdQfY
+lBCbYcYFmy313p/GPo8GdAaiFTKEKfydE9FMCygmoxrBHgnqHAWC0J0UTuipRyb
9EoZ65wKx2nkc99b4wCe9QeSXLjNYESr8lE6CwvALU6TfVu/nf6p9ZXztOPTfRKW
6+eSY8FQqPLxfAJymAsfJw6U+WFVGBP74jX4tt1sZESma+KKZQNj0PK9lSv/wKA1
ZEpgRs3kPdNRbvWTdAoA9JFHHPQ9skiBq4LLjdFrR5Mg2Sqz1BCxxXZwbWdQEOWm
aY/NA5IdNK6KeROtu+X1t7TqVi3kUR9wtj+Bi3SAagECgYEA8vaNQcF6WCB+Z59T
5hGOn2wFAYwqB0+hiEzzNy3bLYfWkMgS9Jcj6FbqdfhA+jTML1pi4Zkq8EHcGUI5
Us2bN/XZy4y5DNGY1hWLeMN18tJTH102Z3FqdoeHNC3YB3wi253vG4aAZPUbmR6S
kNr3YqVQsmOCfSHQdGScZNoMsoECgYEA7C/2qf3o5l2DSd3DRG3or+bSh1Tj93GG
Tp0CHJpW4K8xzzwW5wJ6fk+O4NN19CSR/YE+zg3VWOUZhk2stQ4iZSmtC6RZIv2U
XuQGyLM7aqf9n2qFG5r3kyQjg161497HA0BalgR4/HFpzk/DG2Jo7QOThOJVrOHe
4cVJCx3SFH0CgYEAtOKKWCJLi8DlWxBrziXUISyyrWxE/hxzDp77lGE3hLZVMIVE
V4UO3rOW6y1gcudL/RU1+O+n4CeoTcXYF6WrogYPmFO0ka6aMwjnRYmADsA30mn7
TxhJQuWz600WQLxS117F3aBNhtxVJ7JzPBVJiM+7PJSJWdAK+hzNsugD/AECgYAN
7r5kRMAqZrXJ87UDImCpj7o3lYBlJmM/2+819LzPQEZ17RuEwRaswNCy3oaEwmuC
Qs+LpDFDrzAURhy8CXtp8E1u9GD1uXO5LUZhLIGCxyok70mu2TNlkKovo7SjHo8q
1+8ADn98lMwjCX1+7g02fhGDsz5OlnxpYRPv/fBYmQKBgQDIH8uljwUh8TZ2lU/L
NFijQCCo//d9yyusudDhy/maV5gY2daQf9HAh658hqT5rdBE1Te468E/m7O6cR3H
x8Shm9d4yhGj26Auo/BmVEP+N7zHQORMx1E4d58TFrPMTQmYfciIfyR0d2Zm3nEn
QKzHAh30+zz/hRbtt1EgmwvJTw==
-----END PRIVATE KEY-----
</key>
<ca>
-----BEGIN CERTIFICATE-----
MIIDMjCCAhqgAwIBAgIUNKH49yVUT6GqQloqIkHGlKFsq1swDQYJKoZIhvcNAQEL
BQAwDTELMAkGA1UEAwwCQ0EwIBcNMjExMjI3MDgzMjI4WhgPMjA5MDAxMTQwODMy
MjhaMA0xCzAJBgNVBAMMAkNBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
AQEA01ruJJUQP/K+R/DzeEi7DA8BlQDEtgmtBnHuZsnllqWU6Ie3qVdE4naQU3GS
4TAkVl1cn1Ojq2DEwMlAbyeh6nTMYp9J9Glemg3c/Tei9hDnNSE1PgMyBZ+TXPVc
MNikSb58Tv4SB50CQ2Zcg+vX8O9h4gW7NttMlyaRmfJ42oUXURIG93gosQVDeOXg
QGo5B13msKkXFuACWOHEWBq68r+jZefh7dbTx+LS0wxCoPNlntMmHECvmrfaFwBT
4s96NsbJRgnAtw8jR0g9JqVJH2mblke/wsljysqueb5aFGYtXQ/LnkgPUh7h+MeW
UuSVdw6tVWdM1z67jQzmQXljkwIDAQABo4GHMIGEMB0GA1UdDgQWBBTs/L0I4+lJ
VT31mBoXOslN+NydgDBIBgNVHSMEQTA/gBTs/L0I4+lJVT31mBoXOslN+NydgKER
pA8wDTELMAkGA1UEAwwCQ0GCFDSh+PclVE+hqkJaKiJBxpShbKtbMAwGA1UdEwQF
MAMBAf8wCwYDVR0PBAQDAgEGMA0GCSqGSIb3DQEBCwUAA4IBAQB6OUj2mNeXkKt7
y++gznXcs6BviCDAMMtj4/p9NRoA1Ra8l7JxyOIEVEUnNz0Nfm1CUPlw6O4F3WMi
qJEkaieNmxPqiWaTIi6qcvJZAg23iCuqDUifOfVeT70vyDYVvCUPAsyvUhhQznUG
INOwy7FF6kBpI7alTO33EeQ0mCL3tiwXdKEk42fkfygr06Ryhix/TcUdS7U0/Y4q
JwyiFjkZBa/sJIG0cqCjeZuhERQa0+7skqvSjLJWIj1yXxX6NUp68FKXGKrJhil9
lVHbnlPv+TF63n80GSbEJEs0FJY7oI0oGHNQCj1dwzlkOH5FLhBeNOWyl8AyeKUk
CqjjCVgg
-----END CERTIFICATE-----
</ca>
+1
View File
@@ -0,0 +1 @@
12345678
+2
View File
@@ -0,0 +1,2 @@
jake.kasper
N@t74han
+2
View File
@@ -0,0 +1,2 @@
jake.kasper
N@t74han
+1
View File
@@ -3,3 +3,4 @@ Flask
requests
gunicorn
paramiko
Jinja2
+158
View File
@@ -0,0 +1,158 @@
"""Helpers for streaming journalctl logs over SSH."""
from __future__ import annotations
import json
import logging
import os
import queue
import shlex
import threading
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Iterable, List
import paramiko
SSH_KEY_PATH = Path(os.getenv("SSH_KEY_PATH", "keys/5G-SSH-Key.pem"))
JQ_FILTER = '.TIMESTAMP + " " + .SYSLOG_IDENTIFIER + " " + (.SUPI // "") + " " + .MESSAGE'
@dataclass
class LogTarget:
host: str
processes: List[str]
hostname: str | None = None
class JournalctlStream:
"""Manage concurrent journalctl streams for multiple hosts."""
def __init__(self, targets: Iterable[LogTarget]) -> None:
self._logger = logging.getLogger(__name__)
self.targets = [t for t in targets if t.processes]
if not self.targets:
raise ValueError("No valid log targets provided")
if not SSH_KEY_PATH.exists():
raise FileNotFoundError(f"SSH key not found at {SSH_KEY_PATH}")
self._queue: "queue.Queue[dict]" = queue.Queue()
self._stop_event = threading.Event()
self._threads: list[threading.Thread] = []
self._clients: dict[str, paramiko.SSHClient] = {}
def start(self) -> None:
for target in self.targets:
thread = threading.Thread(target=self._stream_host, args=(target,), daemon=True)
thread.start()
self._threads.append(thread)
def iter_events(self):
self.start()
finished = 0
total = len(self.targets)
while finished < total and not self._stop_event.is_set():
try:
event = self._queue.get(timeout=0.5)
except queue.Empty:
yield {"type": "heartbeat", "timestamp": datetime.now(timezone.utc).isoformat()}
continue
if event.get("type") == "complete":
finished += 1
yield event
# Drain remaining events if any
while not self._queue.empty():
yield self._queue.get()
def stop(self) -> None:
self._stop_event.set()
for client in self._clients.values():
try:
client.close()
except Exception:
pass
for thread in self._threads:
thread.join(timeout=1)
# --- internal helpers ---
def _stream_host(self, target: LogTarget) -> None:
filter_args = []
for proc in target.processes:
proc = (proc or "").strip()
if not proc:
continue
filter_args.append(f"-t {shlex.quote(proc)}")
safe_filters = " ".join(filter_args)
if not safe_filters:
message = "No processes selected"
self._logger.error("Log stream aborted for %s: %s", target.host, message)
self._queue.put({
"type": "error",
"host": target.host,
"hostname": target.hostname or target.host,
"message": message
})
self._queue.put({"type": "complete", "host": target.host, "hostname": target.hostname or target.host})
return
pipeline = (
f"journalctl {safe_filters} -o json -n 50 -f "
f"| jq -r --unbuffered '{JQ_FILTER}'"
)
command = f"bash -lc {shlex.quote(pipeline)}"
self._logger.debug("Executing log command on %s: %s", target.host, pipeline)
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
client.connect(
target.host,
username="root",
key_filename=str(SSH_KEY_PATH),
look_for_keys=False,
timeout=15,
)
self._clients[target.host] = client
_, stdout, stderr = client.exec_command(command, get_pty=True)
for line in iter(lambda: stdout.readline(), ""):
if self._stop_event.is_set():
break
payload = line.rstrip("\r\n")
if not payload:
continue
self._queue.put({
"type": "log",
"host": target.host,
"hostname": target.hostname or target.host,
"line": payload,
"timestamp": datetime.now(timezone.utc).isoformat(),
})
err = stderr.read().decode(errors="ignore").strip()
exit_status = stdout.channel.recv_exit_status()
if exit_status not in (0, -1) or err:
message = err or f"journalctl exited with status {exit_status}"
self._logger.error("Log stream error on %s: %s", target.host, message)
self._queue.put({
"type": "error",
"host": target.host,
"hostname": target.hostname or target.host,
"message": message,
})
except Exception as exc:
message = str(exc)
self._logger.exception("Log stream exception for %s: %s", target.host, message)
self._queue.put({
"type": "error",
"host": target.host,
"hostname": target.hostname or target.host,
"message": message,
})
finally:
self._queue.put({
"type": "complete",
"host": target.host,
"hostname": target.hostname or target.host,
})
try:
client.close()
except Exception:
pass
+194
View File
@@ -0,0 +1,194 @@
"""Helpers for managing OpenVPN processes inside the container."""
from __future__ import annotations
import os
import signal
import subprocess
import time
from pathlib import Path
VPN_CONFIG_DIR = Path(os.getenv("VPN_CONFIG_DIR", "/vpn/configs"))
VPN_RUNTIME_DIR = Path(os.getenv("VPN_RUNTIME_DIR", "/vpn/runtime"))
PID_FILE = VPN_RUNTIME_DIR / "openvpn.pid"
STATE_FILE = VPN_RUNTIME_DIR / "active_vpn"
LOG_FILE = VPN_RUNTIME_DIR / "openvpn.log"
START_TIMEOUT = float(os.getenv("VPN_START_TIMEOUT", "15"))
STOP_TIMEOUT = float(os.getenv("VPN_STOP_TIMEOUT", "10"))
AUTH_DIRECTIVE = "auth-user-pass"
class VPNRuntimeError(RuntimeError):
pass
def _ensure_runtime_dir() -> None:
VPN_RUNTIME_DIR.mkdir(parents=True, exist_ok=True)
def _config_path(vpn_name: str) -> Path:
VPN_CONFIG_DIR.mkdir(parents=True, exist_ok=True)
for suffix in (".conf", ".ovpn"):
candidate = VPN_CONFIG_DIR / f"{vpn_name}{suffix}"
if candidate.exists():
return candidate
raise VPNRuntimeError(f"No config found for VPN '{vpn_name}' in {VPN_CONFIG_DIR}")
def _prepare_auth_override(vpn_name: str, config_path: Path) -> list[str]:
"""Return CLI args to supply a sanitized auth-user-pass file if needed."""
try:
lines = config_path.read_text().splitlines()
except FileNotFoundError as exc:
raise VPNRuntimeError(f"Config {config_path} missing: {exc}") from exc
auth_target: Path | None = None
for raw in lines:
line = raw.strip()
if not line or line.startswith("#") or line.startswith(";"):
continue
if not line.lower().startswith(AUTH_DIRECTIVE):
continue
parts = line.split()
if len(parts) < 2:
continue
auth_path = parts[1].strip('"')
candidate = Path(auth_path)
if not candidate.is_absolute():
candidate = config_path.parent / candidate
auth_target = candidate
break
if not auth_target:
return []
if not auth_target.exists():
raise VPNRuntimeError(
f"Auth file {auth_target} referenced by {config_path} does not exist"
)
dest = VPN_RUNTIME_DIR / f"{vpn_name}.auth"
try:
data = auth_target.read_bytes()
except OSError as exc:
raise VPNRuntimeError(f"Failed to read auth file {auth_target}: {exc}") from exc
if not data.strip():
raise VPNRuntimeError(f"Auth file {auth_target} is empty")
try:
dest.write_bytes(data)
dest.chmod(0o600)
except OSError as exc:
raise VPNRuntimeError(f"Failed to stage auth file at {dest}: {exc}") from exc
return ["--auth-user-pass", str(dest)]
def _is_pid_running(pid: int) -> bool:
try:
os.kill(pid, 0)
return True
except OSError:
return False
def _read_pid() -> int | None:
if PID_FILE.exists():
try:
return int(PID_FILE.read_text().strip())
except ValueError:
PID_FILE.unlink(missing_ok=True)
return None
def _write_state(vpn_name: str) -> None:
STATE_FILE.write_text(vpn_name)
def _clear_state() -> None:
PID_FILE.unlink(missing_ok=True)
STATE_FILE.unlink(missing_ok=True)
def get_active_vpn() -> str | None:
pid = _read_pid()
if not pid:
_clear_state()
return None
if not _is_pid_running(pid):
_clear_state()
return None
if STATE_FILE.exists():
return STATE_FILE.read_text().strip() or None
return None
def stop_active_vpn() -> None:
pid = _read_pid()
if not pid:
_clear_state()
return
if not _is_pid_running(pid):
_clear_state()
return
os.kill(pid, signal.SIGTERM)
deadline = time.time() + STOP_TIMEOUT
while time.time() < deadline:
if not _is_pid_running(pid):
_clear_state()
return
time.sleep(0.5)
# escalate
try:
os.kill(pid, signal.SIGKILL)
except ProcessLookupError:
pass
_clear_state()
def start_vpn(vpn_name: str) -> None:
config_path = _config_path(vpn_name)
_ensure_runtime_dir()
extra_args = _prepare_auth_override(vpn_name, config_path)
stop_active_vpn()
cmd = [
"openvpn",
"--config",
str(config_path),
"--daemon",
"--writepid",
str(PID_FILE),
"--log",
str(LOG_FILE),
"--setenv",
"VPN_NAME",
vpn_name,
]
cmd.extend(extra_args)
try:
subprocess.run(cmd, check=True, cwd=str(config_path.parent))
except subprocess.CalledProcessError as exc:
raise VPNRuntimeError(f"OpenVPN failed to start for {vpn_name}: {exc}") from exc
deadline = time.time() + START_TIMEOUT
while time.time() < deadline:
pid = _read_pid()
if pid and _is_pid_running(pid):
_write_state(vpn_name)
return
time.sleep(0.5)
stop_active_vpn()
raise VPNRuntimeError(f"Timed out waiting for OpenVPN to start for {vpn_name}")
def list_available_vpns() -> list[str]:
if not VPN_CONFIG_DIR.exists():
return []
names: list[str] = []
for path in sorted(VPN_CONFIG_DIR.glob("*.conf")):
names.append(path.stem)
for path in sorted(VPN_CONFIG_DIR.glob("*.ovpn")):
if path.stem not in names:
names.append(path.stem)
return names
+73 -8
View File
@@ -2,15 +2,80 @@
"layout": { "name": "preset" },
"elements": {
"nodes": [
{ "data": { "id": "left", "label": "Cell Site" }, "position": { "x": 100, "y": 250 } },
{ "data": { "id": "core", "label": "Core" }, "position": { "x": 400, "y": 250 } },
{ "data": { "id": "dn", "label": "Data Network" }, "position": { "x": 700, "y": 250 } },
{ "data": { "id": "mgmt", "label": "Management" }, "position": { "x": 400, "y": 50 } }
{ "data": { "id": "5gran", "label": "5G RAN" }, "position": { "x": 100, "y": 300 } },
{ "data": { "id": "host", "label": "Host Machine" }, "position": { "x": 400, "y": 250 } },
{ "data": { "id": "core_cpsig", "label": "ngcore-CPSIG", "parent": "host" }, "position": { "x": 400, "y": 150 } },
{ "data": { "id": "core_up", "label": "ngcore-UP", "parent": "host" }, "position": { "x": 400, "y": 300 } },
{ "data": { "id": "dn", "label": "Data Network" }, "position": { "x": 700, "y": 300 } },
{ "data": { "id": "mgmt", "label": "Management" }, "position": { "x": 700, "y": 50 } }
],
"edges": [
{ "data": { "id": "e1", "source": "left", "target": "core", "sourceLabel": "", "targetLabel": "" } },
{ "data": { "id": "e2", "source": "core", "target": "dn", "sourceLabel": "", "targetLabel": "" } },
{ "data": { "id": "e3", "source": "mgmt", "target": "core", "sourceLabel": "", "targetLabel": "" } }
{ "data": { "id": "n4", "source": "core_cpsig", "target": "core_up", "midLabel": "", "sourceLabel": "", "targetLabel": "" } },
{ "data": { "id": "e_ran_core", "source": "core_up", "target": "core_cpsig", "midLabel": "n4", "sourceLabel": "", "targetLabel": "" } },
{ "data": { "id": "e_core_up_5gran", "source": "core_up", "target": "5gran", "midLabel": "n3", "sourceLabel": "", "targetLabel": "" } },
{ "data": { "id": "e_core_dn", "source": "core_up", "target": "dn", "midLabel": "n6", "sourceLabel": "", "targetLabel": "" } },
{ "data": { "id": "e_mgmt_core", "source": "mgmt", "target": "host", "midLabel": "Mgmt Netw", "sourceLabel": "", "targetLabel": "" } },
{ "data": { "id": "e_5gran_core_cpsig", "source": "5gran", "target": "core_cpsig", "midLabel": "n2", "sourceLabel": "", "targetLabel": "" } }
]
}
},
"styles": [
{
"selector": "#host",
"style": {
"width": 180,
"height": 180,
"text-valign": "bottom",
"text-margin-y": 20,
"background-color": "#f8fafc",
"border-color": "#94a3b8",
"border-width": 2,
"font-weight": "600"
}
},
{
"selector": "#core_cpsig",
"style": {
"width": 140,
"height": 60,
"background-color": "#f59e42",
"border-width": 0
}
},
{
"selector": "#core_up",
"style": {
"width": 140,
"height": 60,
"background-color": "#fde047",
"border-width": 0
}
},
{
"selector": "#dn",
"style": {
"width": 100,
"height": 50,
"background-color": "#000000",
"color": "#ffffff",
"border-width": 0
}
},
{
"selector": "#mgmt",
"style": {
"width": 100,
"height": 50,
"border-width": 0
}
},
{
"selector": "#5gran",
"style": {
"width": 100,
"height": 50,
"background-color": "#ef4444",
"border-width": 0
}
}
]
}
+103
View File
@@ -0,0 +1,103 @@
{
"layout": { "name": "preset" },
"elements": {
"nodes": [
{ "data": { "id": "5gran", "label": "4G/5G RAN" }, "position": { "x": 100, "y": 300 } },
{ "data": { "id": "host", "label": "Host Machine" }, "position": { "x": 400, "y": 250 } },
{ "data": { "id": "core_cpsig", "label": "ngcore-CPSIG", "parent": "host" }, "position": { "x": 400, "y": 150 } },
{ "data": { "id": "core_up", "label": "ngcore-UP", "parent": "host" }, "position": { "x": 400, "y": 300 } },
{ "data": { "id": "dn", "label": "Data Network" }, "position": { "x": 700, "y": 300 } },
{ "data": { "id": "mgmt", "label": "Management" }, "position": { "x": 700, "y": 50 } }
],
"edges": [
{
"data": { "id": "n4", "source": "core_cpsig", "target": "core_up", "midLabel": "n4", "sourceLabel": "", "targetLabel": "" },
"classes": "n4_curve_left"
},
{
"data": { "id": "n4_2", "source": "core_cpsig", "target": "core_up", "midLabel": "S5", "sourceLabel": "", "targetLabel": "" },
"classes": "n4_curve_right"
},
{ "data": { "id": "e_core_up_5gran", "source": "core_up", "target": "5gran", "midLabel": "S1-U / n3", "sourceLabel": "", "targetLabel": "" } },
{ "data": { "id": "e_core_dn", "source": "core_up", "target": "dn", "midLabel": "SGi / n6", "sourceLabel": "", "targetLabel": "" } },
{ "data": { "id": "e_mgmt_core", "source": "mgmt", "target": "host", "midLabel": "Mgmt Netw", "sourceLabel": "", "targetLabel": "" } },
{ "data": { "id": "e_5gran_core_cpsig", "source": "5gran", "target": "core_cpsig", "midLabel": "S1-MME / n2", "sourceLabel": "", "targetLabel": "" } }
]
},
"styles": [
{
"selector": "#host",
"style": {
"width": 180,
"height": 180,
"text-valign": "bottom",
"text-margin-y": 20,
"background-color": "#f8fafc",
"border-color": "#94a3b8",
"border-width": 2,
"font-weight": "600"
}
},
{
"selector": "#core_cpsig",
"style": {
"width": 140,
"height": 60,
"background-color": "#f59e42",
"border-width": 0
}
},
{
"selector": "#core_up",
"style": {
"width": 140,
"height": 60,
"background-color": "#fde047",
"border-width": 0
}
},
{
"selector": "#dn",
"style": {
"width": 100,
"height": 50,
"background-color": "#000000",
"color": "#ffffff",
"border-width": 0
}
},
{
"selector": "#mgmt",
"style": {
"width": 100,
"height": 50,
"border-width": 0
}
},
{
"selector": "#5gran",
"style": {
"width": 100,
"height": 50,
"background-color": "#ef4444",
"border-width": 0
}
},
{
"selector": ".n4_curve_left",
"style": {
"curve-style": "unbundled-bezier",
"control-point-distances": [-80],
"control-point-weights": [0.5]
}
},
{
"selector": ".n4_curve_right",
"style": {
"curve-style": "unbundled-bezier",
"control-point-distances": [80],
"control-point-weights": [0.5]
}
}
]
}
+132
View File
@@ -0,0 +1,132 @@
{
"layout": { "name": "preset" },
"elements": {
"nodes": [
{ "data": { "id": "5gran", "label": "4G/5G RAN" }, "position": { "x": 100, "y": 300 } },
{ "data": { "id": "host", "label": "Host Machine 1" }, "position": { "x": 400, "y": 250 } },
{ "data": { "id": "core_cpsig", "label": "ngcore-CPSIG", "parent": "host" }, "position": { "x": 400, "y": 150 } },
{ "data": { "id": "core_up", "label": "ngcore-UP", "parent": "host" }, "position": { "x": 400, "y": 300 } },
{ "data": { "id": "dn", "label": "Data Network" }, "position": { "x": 950, "y": 300 } },
{ "data": { "id": "mgmt", "label": "Management" }, "position": { "x": 525, "y": 0 } },
{ "data": { "id": "host_2", "label": "Host Machine 2" }, "position": { "x": 650, "y": 250 } },
{ "data": { "id": "core_cpsig_2", "label": "ngcore-CPSIG", "parent": "host_2" }, "position": { "x": 650, "y": 150 } },
{ "data": { "id": "core_up_2", "label": "ngcore-UP", "parent": "host_2" }, "position": { "x": 650, "y": 300 } }
],
"edges": [
{ "data": { "id": "e_core_up_5gran", "source": "core_up", "target": "5gran", "midLabel": "S1-U / n3", "sourceLabel": "", "targetLabel": "" } },
{ "data": { "id": "e_core_dn", "source": "core_up_2", "target": "dn", "midLabel": "SGi / n6", "sourceLabel": "", "targetLabel": "" } },
{ "data": { "id": "e_mgmt_core", "source": "mgmt", "target": "host", "midLabel": "Mgmt Netw", "sourceLabel": "", "targetLabel": "" } },
{ "data": { "id": "e_5gran_core_cpsig", "source": "5gran", "target": "core_cpsig", "midLabel": "S1-MME / n2", "sourceLabel": "", "targetLabel": "" } },
{ "data": { "id": "e_core_up_5gran_2", "source": "core_up_2", "target": "5gran", "midLabel": "S1-U / n3", "sourceLabel": "", "targetLabel": "" } },
{ "data": { "id": "e_core_dn_2", "source": "core_up_2", "target": "dn", "midLabel": "SGi / n6", "sourceLabel": "", "targetLabel": "" } },
{ "data": { "id": "e_mgmt_core_2", "source": "mgmt", "target": "host_2", "midLabel": "Mgmt Netw", "sourceLabel": "", "targetLabel": "" } }
]
},
"styles": [
{
"selector": "#host",
"style": {
"width": 180,
"height": 180,
"text-valign": "bottom",
"text-margin-y": 20,
"background-color": "#f8fafc",
"border-color": "#94a3b8",
"border-width": 2,
"font-weight": "600"
}
},
{
"selector": "#host_2",
"style": {
"width": 180,
"height": 180,
"text-valign": "bottom",
"text-margin-y": 20,
"background-color": "#f8fafc",
"border-color": "#94a3b8",
"border-width": 2,
"font-weight": "600"
}
},
{
"selector": "#core_cpsig",
"style": {
"width": 140,
"height": 60,
"background-color": "#f59e42",
"border-width": 0
}
},
{
"selector": "#core_cpsig_2",
"style": {
"width": 140,
"height": 60,
"background-color": "#f59e42",
"border-width": 0
}
},
{
"selector": "#core_up",
"style": {
"width": 140,
"height": 60,
"background-color": "#fde047",
"border-width": 0
}
},
{
"selector": "#core_up_2",
"style": {
"width": 140,
"height": 60,
"background-color": "#fde047",
"border-width": 0
}
},
{
"selector": "#dn",
"style": {
"width": 100,
"height": 50,
"background-color": "#000000",
"color": "#ffffff",
"border-width": 0
}
},
{
"selector": "#mgmt",
"style": {
"width": 100,
"height": 50,
"border-width": 0
}
},
{
"selector": "#5gran",
"style": {
"width": 100,
"height": 50,
"background-color": "#ef4444",
"border-width": 0
}
},
{
"selector": ".n4_curve_left",
"style": {
"curve-style": "unbundled-bezier",
"control-point-distances": [-80],
"control-point-weights": [0.5]
}
},
{
"selector": ".n4_curve_right",
"style": {
"curve-style": "unbundled-bezier",
"control-point-distances": [80],
"control-point-weights": [0.5]
}
}
]
}
@@ -0,0 +1,132 @@
{
"layout": { "name": "preset" },
"elements": {
"nodes": [
{ "data": { "id": "5gran", "label": "4G/5G RAN" }, "position": { "x": 100, "y": 300 } },
{ "data": { "id": "host", "label": "Host Machine 1" }, "position": { "x": 400, "y": 250 } },
{ "data": { "id": "core_cpsig", "label": "ngcore-CPSIG", "parent": "host" }, "position": { "x": 400, "y": 150 } },
{ "data": { "id": "core_up", "label": "ngcore-UP", "parent": "host" }, "position": { "x": 400, "y": 300 } },
{ "data": { "id": "dn", "label": "Data Network" }, "position": { "x": 950, "y": 300 } },
{ "data": { "id": "mgmt", "label": "Management" }, "position": { "x": 525, "y": 0 } },
{ "data": { "id": "host_2", "label": "Host Machine 2" }, "position": { "x": 650, "y": 250 } },
{ "data": { "id": "core_cpsig_2", "label": "ngcore-CPSIG", "parent": "host_2" }, "position": { "x": 650, "y": 150 } },
{ "data": { "id": "core_up_2", "label": "ngcore-UP", "parent": "host_2" }, "position": { "x": 650, "y": 300 } }
],
"edges": [
{ "data": { "id": "e_core_up_5gran", "source": "core_up", "target": "5gran", "midLabel": "S1-U / n3", "sourceLabel": "", "targetLabel": "" } },
{ "data": { "id": "e_core_dn", "source": "core_up_2", "target": "dn", "midLabel": "SGi / n6", "sourceLabel": "", "targetLabel": "" } },
{ "data": { "id": "e_mgmt_core", "source": "mgmt", "target": "host", "midLabel": "Mgmt Netw", "sourceLabel": "", "targetLabel": "" } },
{ "data": { "id": "e_5gran_core_cpsig", "source": "5gran", "target": "core_cpsig", "midLabel": "S1-MME / n2", "sourceLabel": "", "targetLabel": "" } },
{ "data": { "id": "e_core_up_5gran_2", "source": "core_up_2", "target": "5gran", "midLabel": "S1-U / n3", "sourceLabel": "", "targetLabel": "" } },
{ "data": { "id": "e_core_dn_2", "source": "core_up_2", "target": "dn", "midLabel": "SGi / n6", "sourceLabel": "", "targetLabel": "" } },
{ "data": { "id": "e_mgmt_core_2", "source": "mgmt", "target": "host_2", "midLabel": "Mgmt Netw", "sourceLabel": "", "targetLabel": "" } }
]
},
"styles": [
{
"selector": "#host",
"style": {
"width": 180,
"height": 180,
"text-valign": "bottom",
"text-margin-y": 20,
"background-color": "#f8fafc",
"border-color": "#94a3b8",
"border-width": 2,
"font-weight": "600"
}
},
{
"selector": "#host_2",
"style": {
"width": 180,
"height": 180,
"text-valign": "bottom",
"text-margin-y": 20,
"background-color": "#f8fafc",
"border-color": "#94a3b8",
"border-width": 2,
"font-weight": "600"
}
},
{
"selector": "#core_cpsig",
"style": {
"width": 140,
"height": 60,
"background-color": "#f59e42",
"border-width": 0
}
},
{
"selector": "#core_cpsig_2",
"style": {
"width": 140,
"height": 60,
"background-color": "#f59e42",
"border-width": 0
}
},
{
"selector": "#core_up",
"style": {
"width": 140,
"height": 60,
"background-color": "#fde047",
"border-width": 0
}
},
{
"selector": "#core_up_2",
"style": {
"width": 140,
"height": 60,
"background-color": "#fde047",
"border-width": 0
}
},
{
"selector": "#dn",
"style": {
"width": 100,
"height": 50,
"background-color": "#000000",
"color": "#ffffff",
"border-width": 0
}
},
{
"selector": "#mgmt",
"style": {
"width": 100,
"height": 50,
"border-width": 0
}
},
{
"selector": "#5gran",
"style": {
"width": 100,
"height": 50,
"background-color": "#ef4444",
"border-width": 0
}
},
{
"selector": ".n4_curve_left",
"style": {
"curve-style": "unbundled-bezier",
"control-point-distances": [-80],
"control-point-weights": [0.5]
}
},
{
"selector": ".n4_curve_right",
"style": {
"curve-style": "unbundled-bezier",
"control-point-distances": [80],
"control-point-weights": [0.5]
}
}
]
}
@@ -0,0 +1,132 @@
{
"layout": { "name": "preset" },
"elements": {
"nodes": [
{ "data": { "id": "5gran", "label": "4G/5G RAN" }, "position": { "x": 100, "y": 300 } },
{ "data": { "id": "host", "label": "Host Machine 1" }, "position": { "x": 400, "y": 250 } },
{ "data": { "id": "core_cpsig", "label": "ngcore-CPSIG", "parent": "host" }, "position": { "x": 400, "y": 150 } },
{ "data": { "id": "core_up", "label": "ngcore-UP", "parent": "host" }, "position": { "x": 400, "y": 300 } },
{ "data": { "id": "dn", "label": "Data Network" }, "position": { "x": 950, "y": 300 } },
{ "data": { "id": "mgmt", "label": "Management" }, "position": { "x": 525, "y": 0 } },
{ "data": { "id": "host_2", "label": "Host Machine 2" }, "position": { "x": 650, "y": 250 } },
{ "data": { "id": "core_cpsig_2", "label": "ngcore-CPSIG", "parent": "host_2" }, "position": { "x": 650, "y": 150 } },
{ "data": { "id": "core_up_2", "label": "ngcore-UP", "parent": "host_2" }, "position": { "x": 650, "y": 300 } }
],
"edges": [
{ "data": { "id": "e_core_up_5gran", "source": "core_up", "target": "5gran", "midLabel": "S1-U / n3", "sourceLabel": "", "targetLabel": "" } },
{ "data": { "id": "e_core_dn", "source": "core_up_2", "target": "dn", "midLabel": "SGi / n6", "sourceLabel": "", "targetLabel": "" } },
{ "data": { "id": "e_mgmt_core", "source": "mgmt", "target": "host", "midLabel": "Mgmt Netw", "sourceLabel": "", "targetLabel": "" } },
{ "data": { "id": "e_5gran_core_cpsig", "source": "5gran", "target": "core_cpsig", "midLabel": "S1-MME / n2", "sourceLabel": "", "targetLabel": "" } },
{ "data": { "id": "e_core_up_5gran_2", "source": "core_up_2", "target": "5gran", "midLabel": "S1-U / n3", "sourceLabel": "", "targetLabel": "" } },
{ "data": { "id": "e_core_dn_2", "source": "core_up_2", "target": "dn", "midLabel": "SGi / n6", "sourceLabel": "", "targetLabel": "" } },
{ "data": { "id": "e_mgmt_core_2", "source": "mgmt", "target": "host_2", "midLabel": "Mgmt Netw", "sourceLabel": "", "targetLabel": "" } }
]
},
"styles": [
{
"selector": "#host",
"style": {
"width": 180,
"height": 180,
"text-valign": "bottom",
"text-margin-y": 20,
"background-color": "#f8fafc",
"border-color": "#94a3b8",
"border-width": 2,
"font-weight": "600"
}
},
{
"selector": "#host_2",
"style": {
"width": 180,
"height": 180,
"text-valign": "bottom",
"text-margin-y": 20,
"background-color": "#f8fafc",
"border-color": "#94a3b8",
"border-width": 2,
"font-weight": "600"
}
},
{
"selector": "#core_cpsig",
"style": {
"width": 140,
"height": 60,
"background-color": "#f59e42",
"border-width": 0
}
},
{
"selector": "#core_cpsig_2",
"style": {
"width": 140,
"height": 60,
"background-color": "#f59e42",
"border-width": 0
}
},
{
"selector": "#core_up",
"style": {
"width": 140,
"height": 60,
"background-color": "#fde047",
"border-width": 0
}
},
{
"selector": "#core_up_2",
"style": {
"width": 140,
"height": 60,
"background-color": "#fde047",
"border-width": 0
}
},
{
"selector": "#dn",
"style": {
"width": 100,
"height": 50,
"background-color": "#000000",
"color": "#ffffff",
"border-width": 0
}
},
{
"selector": "#mgmt",
"style": {
"width": 100,
"height": 50,
"border-width": 0
}
},
{
"selector": "#5gran",
"style": {
"width": 100,
"height": 50,
"background-color": "#ef4444",
"border-width": 0
}
},
{
"selector": ".n4_curve_left",
"style": {
"curve-style": "unbundled-bezier",
"control-point-distances": [-80],
"control-point-weights": [0.5]
}
},
{
"selector": ".n4_curve_right",
"style": {
"curve-style": "unbundled-bezier",
"control-point-distances": [80],
"control-point-weights": [0.5]
}
}
]
}
@@ -0,0 +1,138 @@
{
"layout": { "name": "preset" },
"elements": {
"nodes": [
{ "data": { "id": "5gran", "label": "4G/5G RAN" }, "position": { "x": 100, "y": 300 } },
{ "data": { "id": "host", "label": "Host Machine 1" }, "position": { "x": 400, "y": 250 } },
{ "data": { "id": "core_cpsig", "label": "ngcore-CPSIG", "parent": "host" }, "position": { "x": 400, "y": 150 } },
{ "data": { "id": "core_up", "label": "ngcore-UP", "parent": "host" }, "position": { "x": 400, "y": 275 } },
{ "data": { "id": "core_up_1b", "label": "ngcore-UP", "parent": "host" }, "position": { "x": 400, "y": 375 } },
{ "data": { "id": "dn", "label": "Data Network" }, "position": { "x": 950, "y": 300 } },
{ "data": { "id": "mgmt", "label": "Management" }, "position": { "x": 525, "y": 0 } },
{ "data": { "id": "host_2", "label": "Host Machine 2" }, "position": { "x": 650, "y": 250 } },
{ "data": { "id": "core_cpsig_2", "label": "ngcore-CPSIG", "parent": "host_2" }, "position": { "x": 650, "y": 150 } },
{ "data": { "id": "core_up_2", "label": "ngcore-UP", "parent": "host_2" }, "position": { "x": 650, "y": 275 } },
{ "data": { "id": "core_up_2b", "label": "ngcore-UP", "parent": "host_2" }, "position": { "x": 650, "y": 375 } }
],
"edges": [
{ "data": { "id": "e_core_up_5gran", "source": "core_up", "target": "5gran", "midLabel": "S1-U / n3", "sourceLabel": "", "targetLabel": "" } },
{ "data": { "id": "e_core_up_1b_5gran", "source": "core_up_1b", "target": "5gran", "midLabel": "S1-U / n3", "sourceLabel": "", "targetLabel": "" } },
{ "data": { "id": "e_core_dn", "source": "core_up_2", "target": "dn", "midLabel": "SGi / n6", "sourceLabel": "", "targetLabel": "" } },
{ "data": { "id": "e_core_up_2b_dn", "source": "core_up_2b", "target": "dn", "midLabel": "SGi / n6", "sourceLabel": "", "targetLabel": "" } },
{ "data": { "id": "e_mgmt_core", "source": "mgmt", "target": "host", "midLabel": "Mgmt Netw", "sourceLabel": "", "targetLabel": "" } },
{ "data": { "id": "e_5gran_core_cpsig", "source": "5gran", "target": "core_cpsig", "midLabel": "S1-MME / n2", "sourceLabel": "", "targetLabel": "" } },
{ "data": { "id": "e_core_dn_2", "source": "core_up_2", "target": "dn", "midLabel": "SGi / n6", "sourceLabel": "", "targetLabel": "" } },
{ "data": { "id": "e_core_up_2b_dn_2", "source": "core_up_2b", "target": "dn", "midLabel": "SGi / n6", "sourceLabel": "", "targetLabel": "" } },
{ "data": { "id": "e_mgmt_core_2", "source": "mgmt", "target": "host_2", "midLabel": "Mgmt Netw", "sourceLabel": "", "targetLabel": "" } }
]
},
"styles": [
{
"selector": "#host",
"style": {
"width": 180,
"height": 180,
"text-valign": "bottom",
"text-margin-y": 20,
"background-color": "#f8fafc",
"border-color": "#94a3b8",
"border-width": 2,
"font-weight": "600"
}
},
{
"selector": "#host_2",
"style": {
"width": 180,
"height": 180,
"text-valign": "bottom",
"text-margin-y": 20,
"background-color": "#f8fafc",
"border-color": "#94a3b8",
"border-width": 2,
"font-weight": "600"
}
},
{
"selector": "#core_cpsig",
"style": {
"width": 140,
"height": 60,
"background-color": "#f59e42",
"border-width": 0
}
},
{
"selector": "#core_cpsig_2",
"style": {
"width": 140,
"height": 60,
"background-color": "#f59e42",
"border-width": 0
}
},
{
"selector": "#core_up",
"style": {
"width": 140,
"height": 60,
"background-color": "#fde047",
"border-width": 0
}
},
{
"selector": "#core_up_1b",
"style": {
"width": 140,
"height": 60,
"background-color": "#fde047",
"border-width": 0
}
},
{
"selector": "#core_up_2",
"style": {
"width": 140,
"height": 60,
"background-color": "#fde047",
"border-width": 0
}
},
{
"selector": "#core_up_2b",
"style": {
"width": 140,
"height": 60,
"background-color": "#fde047",
"border-width": 0
}
},
{
"selector": "#dn",
"style": {
"width": 100,
"height": 50,
"background-color": "#000000",
"color": "#ffffff",
"border-width": 0
}
},
{
"selector": "#mgmt",
"style": {
"width": 100,
"height": 50,
"border-width": 0
}
},
{
"selector": "#5gran",
"style": {
"width": 100,
"height": 50,
"background-color": "#ef4444",
"border-width": 0
}
}
]
}
+30 -6
View File
@@ -3,10 +3,8 @@
"elements": {
"nodes": [
{ "data": { "id": "ran", "label": "RAN" }, "position": { "x": 100, "y": 240 } },
{ "data": { "id": "proxmox", "label": "Proxmox Host" }, "position": { "x": 450, "y": 240 } },
{ "data": { "id": "core", "label": "All-in-One Core", "parent": "proxmox" }, "position": { "x": 450, "y": 240 } },
{ "data": { "id": "server1", "label": "Host Machine" }, "position": { "x": 450, "y": 240 } },
{ "data": { "id": "core", "label": "4G/5G All-in-One Core", "parent": "server1" }, "position": { "x": 450, "y": 240 } },
{ "data": { "id": "dn", "label": "Data Network" }, "position": { "x": 800, "y": 240 } },
{ "data": { "id": "mgmt", "label": "Management" }, "position": { "x": 450, "y": 0 } }
],
@@ -45,7 +43,7 @@
},
"styles": [
{
"selector": "#proxmox",
"selector": "#server1",
"style": {
"width": 340,
"height": 180,
@@ -59,7 +57,33 @@
},
{
"selector": "#core",
"style": { "width": 140, "height": 70 }
"style": {
"width": 140,
"height": 70,
"background-color": "#22c55e",
"border-width": 0
}
},
{
"selector": "#ran",
"style": {
"background-color": "#ef4444",
"border-width": 0
}
},
{
"selector": "#dn",
"style": {
"background-color": "#000000",
"color": "#ffffff",
"border-width": 0
}
},
{
"selector": "#mgmt",
"style": {
"border-width": 0
}
}
]
}
+1 -1
View File
@@ -16,7 +16,7 @@
"machine_id": "7ebd37b3c5a44ff7acafc84fa3af449d",
"num_cpu": 4,
"virtualization": "kvm",
"target_host_ip": "100.93.1.100",
"target_host_ip": "100.93.0.240",
"mgmt": {
"cidr": "192.168.105.156/24",
"gw": "192.168.105.1"
@@ -50,6 +50,42 @@ _ngc_ext_aio_transport:
n3_address: {{ ran.ip }}
n3_vrf: RAN
# Avoid s-NSSAI on PFCP
- action: set_pfcp_ies
params:
s_nssai: false
# Definition of Network Instances for PFCP
- action: set_pfcp_net_instances
params:
s1_u: RAN
s5s8_u: TELCO
s11_u: TELCO
n3: RAN
n4_u: TELCO
# DNN configuration
_ngc_ext_aio_dnn:
# internet DNN (5G Selection)
- action: add_smf_dnn
params:
dnn: internet
n6_vrf: DN_01
dns:
- 8.8.8.8
# internet DNN (LTE Selection)
- action: add_smf_dnn
params:
dnn: internet
nssai:
sd: ''
sst: 1
n6_vrf: DN_01
dns:
- 8.8.8.8
# DN/DNN (N6) with UE pool
- action: add_n6_dnn
params:
-24
View File
@@ -101,30 +101,6 @@
<div id="vpn-controls">
<h6 class="text-white">Dashboard VPNs</h6>
<ul class="list-unstyled">
<li>
<div class="form-check form-switch text-white">
<input class="form-check-input vpn-toggle" type="checkbox" role="switch" id="vpn-triton" data-vpn-name="Triton">
<label class="form-check-label" for="vpn-triton">Triton</label>
</div>
</li>
<li>
<div class="form-check form-switch text-white">
<input class="form-check-input vpn-toggle" type="checkbox" role="switch" id="vpn-star" data-vpn-name="Star">
<label class="form-check-label" for="vpn-star">Star</label>
</div>
</li>
<li>
<div class="form-check form-switch text-white">
<input class="form-check-input vpn-toggle" type="checkbox" role="switch" id="vpn-bluebonnet" data-vpn-name="Bluebonnet">
<label class="form-check-label" for="vpn-bluebonnet">Bluebonnet</label>
</div>
</li>
<li>
<div class="form-check form-switch text-white">
<input class="form-check-input vpn-toggle" type="checkbox" role="switch" id="vpn-lonestar" data-vpn-name="Lonestar">
<label class="form-check-label" for="vpn-lonestar">Lonestar</label>
</div>
</li>
<li>
<div class="form-check form-switch text-white">
<input class="form-check-input vpn-toggle" type="checkbox" role="switch" id="vpn-production" data-vpn-name="Production">
+10 -43
View File
@@ -143,55 +143,22 @@
</div>
<hr>
<div id="vpn-controls">
<h6 class="text-white">Dashboard VPNs</h6>
<h6 class="text-white">Available VPNs</h6>
{% if available_vpn_configs %}
<ul class="list-unstyled">
<li>
<div class="form-check form-switch text-white">
<input class="form-check-input vpn-toggle" type="checkbox" role="switch" id="vpn-triton" data-vpn-name="Triton">
<label class="form-check-label" for="vpn-triton">Triton</label>
</div>
</li>
{% for vpn_name in available_vpn_configs %}
{% set toggle_id = 'vpn-toggle-' ~ loop.index %}
<li>
<div class="form-check form-switch text-white">
<input class="form-check-input vpn-toggle" type="checkbox" role="switch" id="vpn-star" data-vpn-name="Star">
<label class="form-check-label" for="vpn-star">Star</label>
</div>
</li>
<li>
<div class="form-check form-switch text-white">
<input class="form-check-input vpn-toggle" type="checkbox" role="switch" id="vpn-bluebonnet" data-vpn-name="Bluebonnet">
<label class="form-check-label" for="vpn-bluebonnet">Bluebonnet</label>
</div>
</li>
<li>
<div class="form-check form-switch text-white">
<input class="form-check-input vpn-toggle" type="checkbox" role="switch" id="vpn-lonestar" data-vpn-name="Lonestar">
<label class="form-check-label" for="vpn-lonestar">Lonestar</label>
</div>
</li>
<li>
<div class="form-check form-switch text-white">
<input class="form-check-input vpn-toggle" type="checkbox" role="switch" id="vpn-production" data-vpn-name="Production">
<label class="form-check-label" for="vpn-production">Production</label>
</div>
</li>
</ul>
<hr class="my-2">
<h6 class="text-white">HPE P5G Support VPNs</h6>
<ul class="list-unstyled">
<li>
<div class="form-check form-switch text-white">
<input class="form-check-input vpn-toggle" type="checkbox" role="switch" id="vpn-us-support" data-vpn-name="US-Support">
<label class="form-check-label" for="vpn-us-support">US Support</label>
</div>
</li>
<li>
<div class="form-check form-switch text-white">
<input class="form-check-input vpn-toggle" type="checkbox" role="switch" id="vpn-eu-support" data-vpn-name="EU-Support">
<label class="form-check-label" for="vpn-eu-support">EU Support</label>
<input class="form-check-input vpn-toggle" type="checkbox" role="switch" id="{{ toggle_id }}" data-vpn-name="{{ vpn_name }}">
<label class="form-check-label" for="{{ toggle_id }}">{{ vpn_name }}</label>
</div>
</li>
{% endfor %}
</ul>
{% else %}
<p class="text-white-50 small mb-0">No VPN configs detected.</p>
{% endif %}
</div>
</div>
<div class="b-example-vr"></div>
+7 -2
View File
@@ -399,7 +399,7 @@ document.getElementById('btn-create-yaml').addEventListener('click', async () =>
},
inventory_host: 'GBP08-AIO-1',
esxi_host: 'ESXI-1',
version: '25.1',
version: '25.x',
ova_file: '/home/mjensen/OVA/HPE_ANW_P5G_Core-1.25.1.1-qemux86-64.ova',
// Make sure backend knows which host to use; it will read the fresh snapshot
@@ -416,9 +416,14 @@ document.getElementById('btn-create-yaml').addEventListener('click', async () =>
const j = await res.json();
if (!res.ok || !j.ok) throw new Error(j.error || `HTTP ${res.status}`);
console.log('YAML creation response:', j);
document.getElementById('yaml-badge').textContent = 'Created';
document.getElementById('yaml-badge').className = 'badge bg-success';
alert(`YAML files created in: ${j.staging}`);
if (j.staging) {
alert(`YAML files created in: ${j.staging}`);
} else {
alert('YAML files created, but staging path is missing in response.');
}
} catch (err) {
console.error('Error creating YAML:', err);
document.getElementById('yaml-badge').textContent = 'Failed';
+7 -1
View File
@@ -19,7 +19,13 @@
<dt class="col-sm-5">Hostname</dt><dd class="col-sm-7">{{ details.system.hostname }}</dd>
<dt class="col-sm-5">Customer</dt><dd class="col-sm-7">{{ details.browser_info.customer_name }}</dd>
<dt class="col-sm-5">Common Name</dt><dd class="col-sm-7">{{ details.browser_info.common_name }}</dd>
<dt class="col-sm-5">Virtual IP</dt><dd class="col-sm-7">{{ request.view_args.host_ip }}</dd>
<dt class="col-sm-5">Virtual IP</dt>
<dd class="col-sm-7 d-flex flex-wrap gap-2 align-items-center">
<a href="https://{{ request.view_args.host_ip }}" target="_blank" rel="noopener noreferrer" class="link-light">{{ request.view_args.host_ip }}</a>
<a href="https://{{ request.view_args.host_ip }}" target="_blank" rel="noopener noreferrer" class="btn btn-outline-info btn-sm">
<i class="bi bi-box-arrow-up-right"></i> Open GUI
</a>
</dd>
<dt class="col-sm-5">Public IP</dt><dd class="col-sm-7">{{ details.browser_info.public_ip }}</dd>
<dt class="col-sm-5">Connected Since</dt><dd class="col-sm-7">{{ details.browser_info.connected_since }}</dd>
</dl>
+56 -4
View File
@@ -29,16 +29,68 @@
{% block extra_scripts %}
<script defer>
function renderSupisTable(items = []) {
resultsOutput.innerHTML = '';
if (!Array.isArray(items) || items.length === 0) {
resultsOutput.innerHTML = '<div class="alert alert-info mb-0">No network clients returned for this host.</div>';
return;
}
const preferred = ['supi', 'imsi', 'msisdn', 'profile', 'slice', 'dnn', 'status'];
const columns = preferred.filter(key => items.some(item => item[key] !== undefined));
if (columns.length === 0) {
columns.push(...Object.keys(items[0]).slice(0, 6));
}
const summary = document.createElement('div');
summary.className = 'mb-2 text-white-50';
summary.textContent = `Showing ${items.length} record${items.length === 1 ? '' : 's'}.`;
resultsOutput.appendChild(summary);
const wrapper = document.createElement('div');
wrapper.className = 'table-responsive';
const table = document.createElement('table');
table.className = 'table table-dark table-hover table-sm table-compact align-middle';
const thead = document.createElement('thead');
const headRow = document.createElement('tr');
columns.forEach(col => {
const th = document.createElement('th');
th.textContent = col.replace(/_/g, ' ').toUpperCase();
headRow.appendChild(th);
});
thead.appendChild(headRow);
table.appendChild(thead);
const tbody = document.createElement('tbody');
items.forEach(item => {
const row = document.createElement('tr');
columns.forEach(col => {
const cell = document.createElement('td');
let value = item[col];
if (Array.isArray(value) || (value && typeof value === 'object')) {
value = JSON.stringify(value);
}
cell.textContent = value ?? '';
row.appendChild(cell);
});
tbody.appendChild(row);
});
table.appendChild(tbody);
wrapper.appendChild(table);
resultsOutput.appendChild(wrapper);
}
document.getElementById('listSupiBtn').addEventListener('click', async () => {
const rawHost = document.getElementById('host').value;
const rawHost = document.getElementById('host').value;
if (!rawHost) {
alert('Please enter a 5GC Host IP address.');
return;
}
const host = formatHostIp(rawHost);
const supiData = await apiCall('/api/supis/list', { host });
if (supiData) {
resultsOutput.textContent = JSON.stringify(supiData, null, 2);
const response = await apiCall('/api/supis/list', { host });
if (response && response.supis) {
renderSupisTable(response.supis);
}
});
</script>
+8 -7
View File
@@ -9,13 +9,14 @@
<div class="row align-items-end">
<div class="col-md-4" id="dashboard-select-wrapper">
<label for="dashboard-select" class="form-label">HPE P5G Dashboard</label>
<select class="form-select" id="dashboard-select">
<option selected>Triton</option>
<option>Star</option>
<option>Bluebonnet</option>
<option>Lonestar</option>
<option>Production</option>
<option>Test (future)</option>
<select class="form-select" id="dashboard-select" {% if not dashboard_names %}disabled{% endif %}>
{% if dashboard_names %}
{% for name in dashboard_names %}
<option value="{{ name }}" {% if loop.first %}selected{% endif %}>{{ name }}</option>
{% endfor %}
{% else %}
<option value="">No dashboards configured</option>
{% endif %}
</select>
</div>
</div>
+659 -24
View File
@@ -9,7 +9,10 @@
<hr>
<div id="browser-view">
<p>Retrieve and display live status information for all connected customer networks. You can search, sort, and filter the results.</p>
<button class="btn btn-primary" id="getBrowserDataBtn">Get System Status</button>
<div class="d-flex gap-2">
<button class="btn btn-primary" id="getBrowserDataBtn">Get System Status</button>
<button class="btn btn-outline-info" id="retrieveLogsBtn" disabled>Retrieve Logs</button>
</div>
</div>
<div class="row g-3 my-3 align-items-end">
@@ -29,28 +32,150 @@
</div>
{% endblock %}
<div class="modal fade" id="logWizardModal" tabindex="-1" aria-labelledby="logWizardModalLabel">
<div class="modal-dialog modal-xl modal-dialog-scrollable">
<div class="modal-content bg-dark text-white">
<div class="modal-header">
<h5 class="modal-title" id="logWizardModalLabel">Log Capture Wizard</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div id="wizard-step-process">
<h5>Process Selection</h5>
<p class="text-muted">Only services in a <span class="text-success">started</span> state are listed. Logs will open in a new window.</p>
<div id="process-list" class="d-grid gap-3"></div>
</div>
</div>
<div class="modal-footer">
<span class="text-muted me-auto" id="logWizardStatus"></span>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" id="startLogCaptureBtn" disabled>Start Log Capture</button>
</div>
</div>
</div>
</div>
{% block extra_scripts %}
<script defer>
let fullBrowserData = [];
let currentSort = { column: 'customer_id', direction: 'asc' };
document.addEventListener('DOMContentLoaded', () => {
console.log('[system_browser] script initialized');
const searchInput = document.getElementById('search-input');
const customerFilter = document.getElementById('customer-filter');
const retrieveLogsBtn = document.getElementById('retrieveLogsBtn');
let processListEl = document.getElementById('process-list');
let logWizardStatus = document.getElementById('logWizardStatus');
let startLogCaptureBtn = document.getElementById('startLogCaptureBtn');
let logPlayBtn = null;
let logPauseBtn = null;
let logStopBtn = null;
let logClearBtn = null;
let logWizardModalEl = document.getElementById('logWizardModal');
const spinnerEl = document.getElementById('spinner');
const resultsOutputEl = document.getElementById('results-output');
function injectLogWizardModal() {
const wrapper = document.createElement('div');
wrapper.innerHTML = `
<div class="modal fade" id="logWizardModal" tabindex="-1" aria-labelledby="logWizardModalLabel">
<div class="modal-dialog modal-xl modal-dialog-scrollable">
<div class="modal-content bg-dark text-white">
<div class="modal-header">
<h5 class="modal-title" id="logWizardModalLabel">Log Capture Wizard</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div id="wizard-step-process">
<h5>Process Selection</h5>
<p class="text-muted">Only services in a <span class="text-success">started</span> state are listed. Logs will open in a new window.</p>
<div id="process-list" class="d-grid gap-3"></div>
</div>
</div>
<div class="modal-footer">
<span class="text-muted me-auto" id="logWizardStatus"></span>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" id="startLogCaptureBtn" disabled>Start Log Capture</button>
</div>
</div>
</div>
</div>`;
document.body.appendChild(wrapper.firstElementChild);
processListEl = document.getElementById('process-list');
logWizardStatus = document.getElementById('logWizardStatus');
startLogCaptureBtn = document.getElementById('startLogCaptureBtn');
logWizardModalEl = document.getElementById('logWizardModal');
}
if (!logWizardModalEl || !processListEl || !logWizardStatus || !startLogCaptureBtn) {
injectLogWizardModal();
}
if (!logWizardModalEl || !processListEl || !logWizardStatus || !startLogCaptureBtn) {
console.warn('[system_browser] log wizard elements unavailable; disabling log capture workflow');
return;
}
const logWizardModal = new bootstrap.Modal(logWizardModalEl);
let fullBrowserData = [];
let currentSort = { column: 'customer_id', direction: 'asc' };
let selectedHosts = new Map();
let processLookup = new Map();
let processSelections = new Map();
let logController = null;
let lastLogPayload = null;
let logBuffer = [];
let isStreaming = false;
let logWindow = null;
let logFilterInput = null;
let logOutputEl = null;
let logWindowStatusEl = null;
let logWindowMetaEl = null;
let logFilterTerm = '';
async function postJson(endpoint, body = {}, options = {}) {
const { showSpinner = false, clearResults = false } = options;
if (showSpinner && spinnerEl) {
spinnerEl.classList.remove('d-none');
}
if (clearResults && resultsOutputEl) {
resultsOutputEl.innerHTML = '';
}
try {
const response = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
const result = await response.json();
if (!response.ok) {
const message = result?.error || result?.message || 'Unknown server error';
throw new Error(message);
}
return result;
} catch (error) {
alert(`Error: ${error.message}`);
return null;
} finally {
if (showSpinner && spinnerEl) {
spinnerEl.classList.add('d-none');
}
}
}
function renderTable(data) {
let tableHtml = `<table class="table table-dark table-hover table-sm table-compact">
<thead>
<tr>
<th style="width:2.5rem;"><input class="form-check-input" type="checkbox" id="selectAllHosts"></th>
<th class="tree-item" data-sort="customer_id">Customer ID <i class="bi bi-arrow-down-up"></i></th>
<th class="tree-item" data-sort="customer_name">Customer Name <i class="bi bi-arrow-down-up"></i></th>
<th class="tree-item" data-sort="common_name">Common Name <i class="bi bi-arrow-down-up"></i></th>
<th class="tree-item" data-sort="virtual_ip">Virtual IP <i class="bi bi-arrow-down-up"></i></th>
<th class="tree-item" data-sort="public_ip">Public IP <i class="bi bi-arrow-down-up"></i></th>
</tr>
</thead>
<tbody>`;
data.forEach(client => {
// ADDED: The necessary attributes to make the row clickable
tableHtml += `
<tr class="clickable-row"
data-host-ip="${client.virtual_ip}"
@@ -59,15 +184,56 @@
data-customer-name="${client.customer_name}"
data-common-name="${client.common_name}"
style="cursor: pointer;">
<td>
<input type="checkbox" class="form-check-input host-select" value="${client.virtual_ip}" data-customer="${client.customer_name}" data-common="${client.common_name}" data-public="${client.public_ip}" />
</td>
<td>${client.customer_id}</td>
<td>${client.customer_name}</td>
<td>${client.common_name}</td>
<td><a href="http://${client.virtual_ip}" target="_blank">${client.virtual_ip}</a></td>
<td><a href="https://${client.virtual_ip}" target="_blank" rel="noopener noreferrer">${client.virtual_ip}</a></td>
<td>${client.public_ip || 'N/A'}</td>
</tr>`;
});
tableHtml += '</tbody></table>';
resultsOutput.innerHTML = tableHtml;
if (resultsOutputEl) {
resultsOutputEl.innerHTML = tableHtml;
}
wireHostSelection(data);
}
function wireHostSelection(tableData) {
selectedHosts.clear();
updateRetrieveLogsButton();
const selectAll = document.getElementById('selectAllHosts');
const boxes = resultsOutputEl ? resultsOutputEl.querySelectorAll('.host-select') : [];
boxes.forEach(cb => {
cb.addEventListener('click', (event) => event.stopPropagation());
cb.addEventListener('change', () => {
const hostIp = cb.value;
const record = tableData.find(item => item.virtual_ip === hostIp);
if (cb.checked && record) {
selectedHosts.set(hostIp, record);
} else {
selectedHosts.delete(hostIp);
}
if (selectAll) {
selectAll.checked = boxes.length > 0 && [...boxes].every(box => box.checked);
}
updateRetrieveLogsButton();
});
});
if (selectAll) {
selectAll.addEventListener('change', () => {
boxes.forEach(box => {
box.checked = selectAll.checked;
box.dispatchEvent(new Event('change'));
});
});
}
}
function updateRetrieveLogsButton() {
retrieveLogsBtn.disabled = selectedHosts.size === 0;
}
function populateCustomerFilter(data) {
@@ -85,11 +251,9 @@
let filteredData = [...fullBrowserData];
const searchTerm = searchInput.value.toLowerCase();
const selectedCustomer = customerFilter.value;
if (selectedCustomer) {
filteredData = filteredData.filter(item => item.customer_name === selectedCustomer);
}
if (searchTerm) {
filteredData = filteredData.filter(item =>
Object.values(item).some(value =>
@@ -97,7 +261,6 @@
)
);
}
filteredData.sort((a, b) => {
const valA = a[currentSort.column] || '';
const valB = b[currentSort.column] || '';
@@ -105,12 +268,18 @@
if (valA > valB) return currentSort.direction === 'asc' ? 1 : -1;
return 0;
});
renderTable(filteredData);
}
document.getElementById('getBrowserDataBtn').addEventListener('click', async () => {
const browserData = await apiCall('/api/system-browser/data', {});
const getBrowserDataBtn = document.getElementById('getBrowserDataBtn');
console.log('[system_browser] getBrowserDataBtn found?', !!getBrowserDataBtn);
if (!getBrowserDataBtn) {
return;
}
getBrowserDataBtn.addEventListener('click', async () => {
console.log('[system_browser] Get System Status clicked');
const browserData = await postJson('/api/system-browser/data', {}, { showSpinner: true, clearResults: true });
if (browserData) {
fullBrowserData = browserData;
populateCustomerFilter(fullBrowserData);
@@ -118,11 +287,25 @@
}
});
retrieveLogsBtn.addEventListener('click', async () => {
if (selectedHosts.size === 0) {
alert('Select at least one host.');
return;
}
const hosts = [...selectedHosts.keys()];
const response = await postJson('/api/logs/processes', { hosts }, { showSpinner: true, clearResults: false });
if (response && response.hosts) {
populateProcessList(response.hosts);
startLogCaptureBtn.disabled = !hasAnyProcessSelection();
logWizardStatus.textContent = 'Select the processes you would like to capture logs from. Streams open in a new window after you click Start.';
logWizardModal.show();
}
});
searchInput.addEventListener('input', applyFiltersAndSort);
customerFilter.addEventListener('change', applyFiltersAndSort);
// RESTORED: The full click handler for the results area
resultsOutput.addEventListener('click', (event) => {
resultsOutputEl?.addEventListener('click', (event) => {
const header = event.target.closest('th[data-sort]');
if (header) {
const sortColumn = header.dataset.sort;
@@ -135,19 +318,471 @@
applyFiltersAndSort();
return;
}
if (event.target.closest('.host-select')) {
return;
}
const row = event.target.closest('.clickable-row');
if (row) {
const hostIp = row.dataset.hostIp;
if (hostIp && hostIp !== 'N/A') {
const customerName = encodeURIComponent(row.dataset.customerName);
const commonName = encodeURIComponent(row.dataset.commonName);
const publicIp = encodeURIComponent(row.dataset.publicIp);
const connectedSince = encodeURIComponent(row.dataset.connectedSince);
window.location.href = `/host/${hostIp}?customer_name=${customerName}&common_name=${commonName}&public_ip=${publicIp}&connected_since=${connectedSince}`;
const params = new URLSearchParams({
customer_name: row.dataset.customerName,
common_name: row.dataset.commonName,
public_ip: row.dataset.publicIp,
connected_since: row.dataset.connectedSince
});
window.location.href = `/host/${hostIp}?${params.toString()}`;
}
}
});
function populateProcessList(hostSummaries) {
processLookup.clear();
processSelections.clear();
processListEl.innerHTML = '';
hostSummaries.forEach(summary => {
const card = document.createElement('div');
card.className = 'card bg-secondary bg-opacity-25';
const header = document.createElement('div');
header.className = 'card-header d-flex justify-content-between align-items-center';
header.innerHTML = `<strong>${summary.hostname || summary.host}</strong><span class="badge bg-dark">${summary.host}</span>`;
const body = document.createElement('div');
body.className = 'card-body';
if (summary.error) {
body.innerHTML = `<div class="alert alert-danger mb-0">${summary.error}</div>`;
} else if (!summary.services || summary.services.length === 0) {
body.innerHTML = '<div class="alert alert-warning mb-0">No running services detected.</div>';
} else {
processLookup.set(summary.host, summary);
const defaultSet = new Set(summary.services.map(svc => svc.name));
processSelections.set(summary.host, defaultSet);
const toggleBtn = document.createElement('button');
toggleBtn.className = 'btn btn-sm btn-outline-light mb-2';
toggleBtn.textContent = 'Toggle All';
toggleBtn.addEventListener('click', () => {
const set = processSelections.get(summary.host) || new Set();
if (set.size === summary.services.length) {
set.clear();
} else {
summary.services.forEach(svc => set.add(svc.name));
}
processSelections.set(summary.host, set);
body.querySelectorAll('.process-checkbox').forEach(cb => {
cb.checked = set.has(cb.value);
});
updateStartButtonState();
});
body.appendChild(toggleBtn);
summary.services.forEach(service => {
const id = `${summary.host}-${service.name}`.replace(/[^a-zA-Z0-9_-]/g, '_');
const wrapper = document.createElement('div');
wrapper.className = 'form-check form-switch text-white-50';
wrapper.innerHTML = `
<input class="form-check-input process-checkbox" type="checkbox" id="${id}" value="${service.name}" checked>
<label class="form-check-label" for="${id}">${service.name}</label>
`;
const checkbox = wrapper.querySelector('input');
checkbox.addEventListener('change', () => {
const set = processSelections.get(summary.host) || new Set();
if (checkbox.checked) {
set.add(checkbox.value);
} else {
set.delete(checkbox.value);
}
processSelections.set(summary.host, set);
updateStartButtonState();
});
body.appendChild(wrapper);
});
}
card.appendChild(header);
card.appendChild(body);
processListEl.appendChild(card);
});
updateStartButtonState();
}
function hasAnyProcessSelection() {
for (const set of processSelections.values()) {
if (set.size > 0) {
return true;
}
}
return false;
}
function updateStartButtonState() {
startLogCaptureBtn.disabled = !hasAnyProcessSelection();
}
function resetLogWindowRefs() {
logWindow = null;
logOutputEl = null;
logFilterInput = null;
logWindowStatusEl = null;
logWindowMetaEl = null;
logPlayBtn = null;
logPauseBtn = null;
logStopBtn = null;
logClearBtn = null;
logFilterTerm = '';
}
function buildLogWindowMarkup(targets) {
const hostItems = targets.map(t => {
const procList = (t.processes || []).join(', ');
return `
<li class="list-group-item bg-dark text-white border-secondary">
<div class="d-flex justify-content-between align-items-center">
<strong>${t.hostname || t.host}</strong>
<span class="text-secondary small">${t.host}</span>
</div>
<div class="small text-info">Processes: ${procList}</div>
</li>`;
}).join('');
return `
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Live Log Capture</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<style>
body { background-color: #080a0e; color: #f8f9fa; font-family: "Inter", "Segoe UI", sans-serif; }
pre#log-output { white-space: pre-wrap; word-break: break-word; font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace; font-size: 0.85rem; line-height: 1.35; min-height: 400px; max-height: calc(100vh - 260px); overflow:auto; background-color: #050608; color: #8ee5a1; border: 1px solid rgba(255,255,255,0.08); }
</style>
</head>
<body>
<div class="container-fluid py-3" style="max-width: 1100px;">
<div class="d-flex justify-content-between align-items-center mb-2">
<h3 class="mb-0">Live Log Capture</h3>
<span id="logWindowStatus" class="text-warning small"></span>
</div>
<p id="logWindowMeta" class="text-muted small"></p>
<div class="card bg-secondary bg-opacity-25 mb-3">
<div class="card-body p-2">
<h6 class="text-uppercase small text-muted mb-2">Targets</h6>
<ul class="list-group list-group-flush">${hostItems}</ul>
</div>
</div>
<div class="d-flex flex-wrap gap-2 mb-3">
<button class="btn btn-success btn-sm" id="logPlayBtn" disabled><i class="bi bi-play-fill"></i> Play</button>
<button class="btn btn-warning btn-sm" id="logPauseBtn" disabled><i class="bi bi-pause-fill"></i> Pause</button>
<button class="btn btn-danger btn-sm" id="logStopBtn" disabled><i class="bi bi-stop-fill"></i> Stop</button>
<button class="btn btn-outline-light btn-sm" id="logClearBtn"><i class="bi bi-eraser"></i> Clear</button>
<div class="btn-group">
<button class="btn btn-outline-info btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-download"></i> Export
</button>
<ul class="dropdown-menu dropdown-menu-dark">
<li><a class="dropdown-item" href="#" data-export-format="text">Text</a></li>
<li><a class="dropdown-item" href="#" data-export-format="csv">CSV</a></li>
<li><a class="dropdown-item" href="#" data-export-format="json">JSON</a></li>
</ul>
</div>
<div class="flex-grow-1 min-w-25">
<input type="text" class="form-control form-control-sm" id="logFilterInput" placeholder="Filter logs (live)">
</div>
</div>
<pre id="log-output" class="rounded p-3"></pre>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"><\/script>
</body>
</html>`;
}
function openLogWindow(targets) {
try {
if (logWindow && !logWindow.closed) {
logWindow.close();
}
logWindow = window.open('', 'logCaptureWindow', 'width=1100,height=720,resizable=yes,scrollbars=yes');
} catch (error) {
console.error('Failed to open log window', error);
alert('Unable to open log window. Please allow pop-ups for this site.');
logWindow = null;
return false;
}
if (!logWindow) {
alert('Unable to open log window. Please allow pop-ups for this site.');
return false;
}
logWindow.document.write(buildLogWindowMarkup(targets));
logWindow.document.close();
logWindow.addEventListener('beforeunload', () => {
stopLogStreaming();
resetLogWindowRefs();
});
logOutputEl = logWindow.document.getElementById('log-output');
logFilterInput = logWindow.document.getElementById('logFilterInput');
logWindowStatusEl = logWindow.document.getElementById('logWindowStatus');
logWindowMetaEl = logWindow.document.getElementById('logWindowMeta');
logPlayBtn = logWindow.document.getElementById('logPlayBtn');
logPauseBtn = logWindow.document.getElementById('logPauseBtn');
logStopBtn = logWindow.document.getElementById('logStopBtn');
logClearBtn = logWindow.document.getElementById('logClearBtn');
logWindow.document.querySelectorAll('[data-export-format]').forEach(link => {
link.addEventListener('click', (event) => {
event.preventDefault();
exportLogs(link.dataset.exportFormat);
});
});
if (logFilterInput) {
logFilterInput.addEventListener('input', (event) => {
logFilterTerm = (event.target.value || '').toLowerCase();
renderLogBuffer(false);
});
}
logPlayBtn?.addEventListener('click', handleLogPlay);
logPauseBtn?.addEventListener('click', handleLogPause);
logStopBtn?.addEventListener('click', handleLogStop);
logClearBtn?.addEventListener('click', handleLogClear);
const hostSummary = `${targets.length} host(s) Processes: ${[...new Set(targets.flatMap(t => t.processes))].join(', ')}`;
if (logWindowMetaEl) {
logWindowMetaEl.textContent = hostSummary;
}
logFilterTerm = '';
renderLogBuffer(false);
updateLogControls();
setLogWindowStatus('Log window ready.');
logWindow.focus();
return true;
}
function setLogWindowStatus(message) {
if (logWindowStatusEl && logWindow && !logWindow.closed) {
logWindowStatusEl.textContent = message;
} else if (logWizardStatus) {
logWizardStatus.textContent = message;
}
}
function renderLogBuffer(scrollToEnd = true) {
if (!logOutputEl || !logWindow || logWindow.closed) {
return;
}
const normalizedFilter = logFilterTerm;
const filtered = normalizedFilter
? logBuffer.filter(entry =>
entry.line.toLowerCase().includes(normalizedFilter) ||
(entry.hostname || entry.host || '').toLowerCase().includes(normalizedFilter))
: logBuffer;
logOutputEl.textContent = filtered.map(entry => `${entry.timestamp} [${entry.hostname || entry.host}] ${entry.line}`).join('\n');
if (scrollToEnd) {
logOutputEl.scrollTop = logOutputEl.scrollHeight;
}
}
function handleLogPlay() {
if (lastLogPayload) {
setLogWindowStatus('Resuming log capture...');
startLogStreaming(lastLogPayload);
}
}
function handleLogPause() {
stopLogStreaming(false);
setLogWindowStatus('Log capture paused.');
}
function handleLogStop() {
stopLogStreaming();
setLogWindowStatus('Log capture stopped.');
}
function handleLogClear() {
logBuffer = [];
renderLogBuffer(false);
}
startLogCaptureBtn.addEventListener('click', () => {
const targets = buildLogTargets();
if (!targets.length) {
alert('Select at least one process.');
return;
}
if (!openLogWindow(targets)) {
setLogWindowStatus('Pop-up blocked. Please allow pop-ups and try again.');
return;
}
logBuffer = [];
renderLogBuffer(false);
setLogWindowStatus('Starting log capture...');
logWizardModal.hide();
startLogStreaming({ targets });
});
function buildLogTargets() {
const targets = [];
processSelections.forEach((set, host) => {
if (set.size > 0) {
const meta = processLookup.get(host) || {};
targets.push({ host, hostname: meta.hostname, processes: [...set] });
}
});
return targets;
}
function startLogStreaming(payload) {
stopLogStreaming(false);
lastLogPayload = payload;
isStreaming = true;
updateLogControls();
setLogWindowStatus('Streaming logs...');
logController = new AbortController();
fetch('/api/logs/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
signal: logController.signal,
}).then(response => {
if (!response.ok) {
throw new Error('Failed to start log stream');
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
const readChunk = () => {
reader.read().then(({ value, done }) => {
if (done) {
isStreaming = false;
updateLogControls();
setLogWindowStatus('Log stream ended.');
return;
}
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop();
lines.forEach(line => {
if (!line.trim()) return;
try {
handleLogEvent(JSON.parse(line));
} catch (err) {
console.error('Failed to parse log event', err, line);
}
});
readChunk();
}).catch(err => {
if (err.name === 'AbortError') {
setLogWindowStatus('Log capture paused.');
} else {
console.error(err);
setLogWindowStatus(`Log stream error: ${err.message}`);
}
isStreaming = false;
updateLogControls();
});
};
readChunk();
}).catch(err => {
if (err.name !== 'AbortError') {
alert(err.message);
}
isStreaming = false;
updateLogControls();
setLogWindowStatus(`Log stream error: ${err.message}`);
});
}
function handleLogEvent(event) {
if (event.type === 'heartbeat') {
return;
}
if (event.type === 'log') {
logBuffer.push({
timestamp: event.timestamp,
host: event.host,
hostname: event.hostname,
line: event.line,
});
} else if (event.type === 'error') {
logBuffer.push({
timestamp: event.timestamp || new Date().toISOString(),
host: event.host,
hostname: event.hostname,
line: `ERROR: ${event.message}`,
});
} else if (event.type === 'complete') {
logBuffer.push({
timestamp: event.timestamp || new Date().toISOString(),
host: event.host,
hostname: event.hostname,
line: 'stream completed.',
});
}
appendLogLine();
}
function appendLogLine() {
renderLogBuffer(true);
}
function stopLogStreaming(clearPayload = true) {
if (logController) {
logController.abort();
logController = null;
}
if (clearPayload) {
lastLogPayload = null;
}
isStreaming = false;
updateLogControls();
if (clearPayload) {
setLogWindowStatus('Log capture stopped.');
}
}
function updateLogControls() {
if (logPlayBtn) {
logPlayBtn.disabled = isStreaming || !lastLogPayload;
}
if (logPauseBtn) {
logPauseBtn.disabled = !isStreaming;
}
if (logStopBtn) {
logStopBtn.disabled = !lastLogPayload;
}
}
function exportLogs(format) {
if (!logBuffer.length) {
alert('No logs to export.');
return;
}
let mime = 'text/plain';
let content = '';
if (format === 'json') {
mime = 'application/json';
content = JSON.stringify(logBuffer, null, 2);
} else if (format === 'csv') {
mime = 'text/csv';
const header = 'timestamp,host,hostname,line';
const rows = logBuffer.map(entry => [entry.timestamp, entry.host, entry.hostname || '', entry.line.replace(/"/g, '""')].map(val => `"${val || ''}"`).join(','));
content = [header, ...rows].join('\n');
} else {
content = logBuffer.map(entry => `${entry.timestamp} [${entry.hostname || entry.host}] ${entry.line}`).join('\n');
}
const blob = new Blob([content], { type: mime });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `logs.${format === 'text' ? 'txt' : format}`;
a.click();
URL.revokeObjectURL(url);
}
logWizardModalEl.addEventListener('hidden.bs.modal', () => {
if (!isStreaming) {
logBuffer = [];
}
if (logWizardStatus) {
logWizardStatus.textContent = '';
}
});
});
</script>
{% endblock %}
+8 -7
View File
@@ -9,13 +9,14 @@
<div class="row align-items-end">
<div class="col-md-4" id="dashboard-select-wrapper">
<label for="dashboard-select" class="form-label">HPE P5G Dashboard</label>
<select class="form-select" id="dashboard-select">
<option selected>Triton</option>
<option>Star</option>
<option>Bluebonnet</option>
<option>Lonestar</option>
<option>Production</option>
<option>Test (future)</option>
<select class="form-select" id="dashboard-select" {% if not dashboard_names %}disabled{% endif %}>
{% if dashboard_names %}
{% for name in dashboard_names %}
<option value="{{ name }}" {% if loop.first %}selected{% endif %}>{{ name }}</option>
{% endfor %}
{% else %}
<option value="">No dashboards configured</option>
{% endif %}
</select>
</div>
</div>
+8 -7
View File
@@ -9,13 +9,14 @@
<div class="row align-items-end">
<div class="col-md-4" id="dashboard-select-wrapper">
<label for="dashboard-select" class="form-label">HPE P5G Dashboard</label>
<select class="form-select" id="dashboard-select">
<option selected>Triton</option>
<option>Star</option>
<option>Bluebonnet</option>
<option>Lonestar</option>
<option>Production</option>
<option>Test (future)</option>
<select class="form-select" id="dashboard-select" {% if not dashboard_names %}disabled{% endif %}>
{% if dashboard_names %}
{% for name in dashboard_names %}
<option value="{{ name }}" {% if loop.first %}selected{% endif %}>{{ name }}</option>
{% endfor %}
{% else %}
<option value="">No dashboards configured</option>
{% endif %}
</select>
</div>
</div>
+98 -7
View File
@@ -9,13 +9,14 @@
<div class="row align-items-end">
<div class="col-md-4" id="dashboard-select-wrapper">
<label for="dashboard-select" class="form-label">HPE P5G Dashboard</label>
<select class="form-select" id="dashboard-select">
<option selected>Triton</option>
<option>Star</option>
<option>Bluebonnet</option>
<option>Lonestar</option>
<option>Production</option>
<option>Test (future)</option>
<select class="form-select" id="dashboard-select" {% if not dashboard_names %}disabled{% endif %}>
{% if dashboard_names %}
{% for name in dashboard_names %}
<option value="{{ name }}" {% if loop.first %}selected{% endif %}>{{ name }}</option>
{% endfor %}
{% else %}
<option value="">No dashboards configured</option>
{% endif %}
</select>
</div>
</div>
@@ -29,7 +30,28 @@
<h4>Results</h4>
<div id="spinner" class="d-none spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div>
<div id="results-output" class="p-3"></div>
<!-- Serial and IPv6 subnet input on the same line, styled -->
<div class="mt-3">
<label class="form-label">Restart VPN by Serial & IPv6 Subnet</label>
<div class="input-group mb-2">
<input type="text" id="serial-input" class="form-control" placeholder="Serial Number"
style="background:#000;color:#fff;font-size:1.2em;border:2px solid #fff;width:10vw;">
<input type="text" id="ipv6-subnet-input" class="form-control ms-2" placeholder="IPv6 prefix"
style="background:#000;color:#fff;font-size:1.2em;border:2px solid #fff;width:10vw;">
<button class="btn btn-danger ms-2" id="restart-ipv6-btn" type="button">Restart VPN</button>
</div>
<div style="margin-top: 1.5em;"></div>
<div id="serial-password-result" class="mt-2"></div>
</div>
</div>
<style>
#serial-input::placeholder,
#ipv6-subnet-input::placeholder {
color: #6b7280 !important; /* Tailwind slate-500 */
opacity: 1;
font-weight: normal !important;
}
</style>
{% endblock %}
{% block extra_scripts %}
@@ -76,5 +98,74 @@
actionLink.innerHTML = `<i class="bi bi-arrow-clockwise"></i> Restart`;
}
});
// Helper to safely call API and handle non-JSON errors
async function apiCall(url, data, showAlert = true) {
try {
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const text = await res.text();
try {
return JSON.parse(text);
} catch (e) {
if (showAlert) {
alert("Error: Server returned invalid response. Check server logs.");
}
return null;
}
} catch (err) {
if (showAlert) {
alert("Error: Could not reach server.");
}
return null;
}
}
document.getElementById('restart-ipv6-btn').addEventListener('click', async () => {
const serial = document.getElementById('serial-input').value.trim();
const subnet = document.getElementById('ipv6-subnet-input').value.trim();
const dashboard = document.getElementById('dashboard-select').value;
const btn = document.getElementById('restart-ipv6-btn');
const resultDiv = document.getElementById('serial-password-result');
// Validate input
if (!serial) {
alert('Please enter a Serial.');
return;
}
if (!subnet || !subnet.match(/^fd[0-9a-fA-F:]+\/\d+$/)) {
alert('Please enter a valid IPv6 subnet (e.g. fd14:6666::45:0/112).');
return;
}
btn.disabled = true;
btn.innerHTML = `<span class="spinner-border spinner-border-sm"></span> Sending...`;
resultDiv.innerHTML = '';
// Lookup password first
const pwResult = await apiCall('/api/m2000/lookup_password', { dashboard, serial }, false);
if (pwResult && pwResult.password) {
resultDiv.innerHTML = `<strong>Password:</strong> <code>${pwResult.password}</code>`;
} else if (pwResult && pwResult.error) {
resultDiv.innerHTML = `<span class="text-danger">Password not found.</span>`;
} else {
// Do not show anything if the API failed silently
resultDiv.innerHTML = '';
}
// Restart VPN
const result = await apiCall('/api/m2000/restart', { dashboard, serial, subnet }, false);
if (result && result.message) {
alert(result.message);
} else if (!result) {
alert("Error: Could not restart VPN. Check server logs.");
}
btn.disabled = false;
btn.innerHTML = 'Restart VPN';
});
</script>
{% endblock %}