Compare commits
4 Commits
aba339fa10
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 59d8db92ca | |||
| 2ed785e214 | |||
| 7875b06fd6 | |||
| b9d2d762b6 |
@@ -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
|
||||
@@ -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
@@ -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
@@ -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,7 +1,7 @@
|
||||
all:
|
||||
hosts:
|
||||
GBP08-AIO-1:
|
||||
ansible_host: 100.93.1.100
|
||||
ansible_host: 100.93.0.240
|
||||
children:
|
||||
ESXi:
|
||||
hosts:
|
||||
|
||||
@@ -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
@@ -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
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
Executable
+38
@@ -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-----
|
||||
@@ -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>
|
||||
Executable
+115
@@ -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>
|
||||
@@ -0,0 +1 @@
|
||||
12345678
|
||||
@@ -0,0 +1,2 @@
|
||||
jake.kasper
|
||||
N@t74han
|
||||
@@ -0,0 +1,2 @@
|
||||
jake.kasper
|
||||
N@t74han
|
||||
@@ -3,3 +3,4 @@ Flask
|
||||
requests
|
||||
gunicorn
|
||||
paramiko
|
||||
Jinja2
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
Reference in New Issue
Block a user