diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..cebd69d --- /dev/null +++ b/.dockerignore @@ -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 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a23f280 --- /dev/null +++ b/.env.example @@ -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 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0261505 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/Triton.conf b/Triton.conf deleted file mode 100644 index a7809b2..0000000 --- a/Triton.conf +++ /dev/null @@ -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 - - ------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----- - - - ------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----- - - ------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----- - diff --git a/ansible_workspace/staging/host_vars/ESXI-1/esxi.yaml b/ansible_workspace/staging/host_vars/ESXI-1/esxi.yaml index 1612f50..aaf1bfa 100644 --- a/ansible_workspace/staging/host_vars/ESXI-1/esxi.yaml +++ b/ansible_workspace/staging/host_vars/ESXI-1/esxi.yaml @@ -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: 5, vlanName: DN_01 } +- { vSwitch: GAF_VSWITCH, vlanId: 2100, vlanName: DN_01 } - { vSwitch: GAF_VSWITCH, vlanId: 4095, vlanName: GAF_BP_T_510_515 } \ No newline at end of file diff --git a/ansible_workspace/staging/host_vars/GBP08-AIO-1/aio_3gpp.yaml b/ansible_workspace/staging/host_vars/GBP08-AIO-1/aio_3gpp.yaml index 5465361..5f9b72e 100644 --- a/ansible_workspace/staging/host_vars/GBP08-AIO-1/aio_3gpp.yaml +++ b/ansible_workspace/staging/host_vars/GBP08-AIO-1/aio_3gpp.yaml @@ -39,15 +39,15 @@ _ngc_ext_aio_transport: # RAN transports (use RAN IP) - action: override_amf_n2_transport - params: { address: 10.5.19.195, vrf: RAN } + params: { address: 10.10.0.2, vrf: RAN } - action: override_mme_transport - params: { s1_address: 10.5.19.195, 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: 10.5.19.195 + n3_address: 10.10.0.2 n3_vrf: RAN # Avoid s-NSSAI on PFCP @@ -91,13 +91,13 @@ _ngc_ext_aio_dnn: params: n6_dnn: internet n6_vrf: DN_01 - n6_vlan: 5 + n6_vlan: 2100 n6_vrf_table: 511 n6_interface: eth2 - n6_ip: 10.5.20.195/24 - n6_gw: 10.5.20.254 + n6_ip: 10.121.0.150/24 + n6_gw: 10.121.0.1 n6_upf_pools: - - upf_route: 172.16.2.0/24 + - upf_route: 192.168.4.0/24 nssai: false n6_bgp: local_as: 65001 diff --git a/ansible_workspace/staging/host_vars/GBP08-AIO-1/aio_networking.yaml b/ansible_workspace/staging/host_vars/GBP08-AIO-1/aio_networking.yaml index 17e9288..bcbcd72 100644 --- a/ansible_workspace/staging/host_vars/GBP08-AIO-1/aio_networking.yaml +++ b/ansible_workspace/staging/host_vars/GBP08-AIO-1/aio_networking.yaml @@ -5,8 +5,8 @@ net_recipe: generic_bgp oam_network: add_ansible_host_address: false addresses: - - 10.5.21.54/24 - gateway4: 10.5.21.254 + - 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: - - 10.5.19.195/23 # S1+N2+N3 + - 10.10.0.2/24 # S1+N2+N3 routes: - destination: 0.0.0.0/0 - gateway: 10.5.19.254 \ No newline at end of file + gateway: 10.10.0.254 \ No newline at end of file diff --git a/ansible_workspace/staging/hosts.yaml b/ansible_workspace/staging/hosts.yaml index 4c2cb64..da76fc8 100644 --- a/ansible_workspace/staging/hosts.yaml +++ b/ansible_workspace/staging/hosts.yaml @@ -1,7 +1,7 @@ all: hosts: GBP08-AIO-1: - ansible_host: 100.93.1.84 + ansible_host: 100.93.0.240 children: ESXi: hosts: diff --git a/app.py b/app.py index 07fceda..e4e796d 100644 --- a/app.py +++ b/app.py @@ -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) @@ -751,4 +890,4 @@ def api_list_users(): return jsonify({"error": str(e)}), 500 if __name__ == "__main__": - app.run(debug=True) \ No newline at end of file + app.run(debug=True) diff --git a/auth_utils.py b/auth_utils.py index a9c513e..cbe3b28 100644 --- a/auth_utils.py +++ b/auth_utils.py @@ -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("["): @@ -34,11 +41,11 @@ def get_vpn_dashboard_token(base_url): "Content-Type": "application/json", "Accept": "*/*", "User-Agent": "Mozilla/5.0" }) - credentials = { - "user": "admin@hpe.com", "password": "JohnWayne#21", - # "user": "admin@hpe.com", "password": "administratoR!1", - # "user": "admin@athonet.com", "password": "administratoR!1", - "lang": "en", "auth_provider": "enterprise" + credentials = { + "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) @@ -56,4 +63,4 @@ def get_vpn_dashboard_token(base_url): if not token: raise ValueError("Login failed: Could not retrieve session token.") - return token, session \ No newline at end of file + return token, session diff --git a/core_functions.py b/core_functions.py index e5a8fa1..62bd0dd 100644 --- a/core_functions.py +++ b/core_functions.py @@ -5,14 +5,25 @@ 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", @@ -207,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" @@ -372,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): diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..2d589cf --- /dev/null +++ b/docker-compose.yml @@ -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: diff --git a/keys/5G-SSH-Key.pem b/keys/5G-SSH-Key.pem new file mode 100755 index 0000000..120d730 --- /dev/null +++ b/keys/5G-SSH-Key.pem @@ -0,0 +1,38 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +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 +-----END OPENSSH PRIVATE KEY----- diff --git a/openvpn/configs/Production.ovpn b/openvpn/configs/Production.ovpn new file mode 100644 index 0000000..6868892 --- /dev/null +++ b/openvpn/configs/Production.ovpn @@ -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 + + +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----- + + + +-----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----- + + + +-----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----- + \ No newline at end of file diff --git a/openvpn/configs/US-Support.ovpn b/openvpn/configs/US-Support.ovpn new file mode 100755 index 0000000..0ee42c8 --- /dev/null +++ b/openvpn/configs/US-Support.ovpn @@ -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 + + +# +# 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----- + + + +-----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----- + + +-----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----- + + +-----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----- + diff --git a/openvpn/configs/cm-prod.auth b/openvpn/configs/cm-prod.auth new file mode 100644 index 0000000..e9a9ea1 --- /dev/null +++ b/openvpn/configs/cm-prod.auth @@ -0,0 +1 @@ +12345678 \ No newline at end of file diff --git a/openvpn/configs/noc-us.auth b/openvpn/configs/noc-us.auth new file mode 100644 index 0000000..f7cd033 --- /dev/null +++ b/openvpn/configs/noc-us.auth @@ -0,0 +1,2 @@ +jake.kasper +N@t74han \ No newline at end of file diff --git a/openvpn/runtime/.gitkeep b/openvpn/runtime/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/openvpn/runtime/US-Support.auth b/openvpn/runtime/US-Support.auth new file mode 100644 index 0000000..f7cd033 --- /dev/null +++ b/openvpn/runtime/US-Support.auth @@ -0,0 +1,2 @@ +jake.kasper +N@t74han \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 2b09d3d..0f1236d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ Flask requests gunicorn paramiko +Jinja2 diff --git a/services/log_stream.py b/services/log_stream.py new file mode 100644 index 0000000..74cb354 --- /dev/null +++ b/services/log_stream.py @@ -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 diff --git a/services/vpn_runtime.py b/services/vpn_runtime.py new file mode 100644 index 0000000..e8333a9 --- /dev/null +++ b/services/vpn_runtime.py @@ -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 diff --git a/system_info.json b/system_info.json index a6b7975..a40bd30 100644 --- a/system_info.json +++ b/system_info.json @@ -16,7 +16,7 @@ "machine_id": "7ebd37b3c5a44ff7acafc84fa3af449d", "num_cpu": 4, "virtualization": "kvm", - "target_host_ip": "100.93.1.84", + "target_host_ip": "100.93.0.240", "mgmt": { "cidr": "192.168.105.156/24", "gw": "192.168.105.1" diff --git a/templates/index.html b/templates/index.html index f6578c0..f25d971 100644 --- a/templates/index.html +++ b/templates/index.html @@ -101,30 +101,6 @@
Dashboard VPNs
@@ -326,4 +293,4 @@ {% block extra_scripts %}{% endblock %} - \ No newline at end of file + diff --git a/templates/pages/host_details.html b/templates/pages/host_details.html index 9474a23..e05aedb 100644 --- a/templates/pages/host_details.html +++ b/templates/pages/host_details.html @@ -19,7 +19,13 @@
Hostname
{{ details.system.hostname }}
Customer
{{ details.browser_info.customer_name }}
Common Name
{{ details.browser_info.common_name }}
-
Virtual IP
{{ request.view_args.host_ip }}
+
Virtual IP
+
+ {{ request.view_args.host_ip }} + + Open GUI + +
Public IP
{{ details.browser_info.public_ip }}
Connected Since
{{ details.browser_info.connected_since }}
@@ -135,4 +141,4 @@ } }); -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/pages/network_clients.html b/templates/pages/network_clients.html index 1e865e8..d9162d3 100644 --- a/templates/pages/network_clients.html +++ b/templates/pages/network_clients.html @@ -29,17 +29,69 @@ {% block extra_scripts %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/pages/network_config.html b/templates/pages/network_config.html index bfa5ea7..0546a55 100644 --- a/templates/pages/network_config.html +++ b/templates/pages/network_config.html @@ -9,13 +9,14 @@
- + {% if dashboard_names %} + {% for name in dashboard_names %} + + {% endfor %} + {% else %} + + {% endif %}
@@ -186,4 +187,4 @@ }); -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/pages/system_browser.html b/templates/pages/system_browser.html index 983ab08..07df9f9 100644 --- a/templates/pages/system_browser.html +++ b/templates/pages/system_browser.html @@ -9,7 +9,10 @@

Retrieve and display live status information for all connected customer networks. You can search, sort, and filter the results.

- +
+ + +
@@ -29,45 +32,208 @@
{% endblock %} + + {% block extra_scripts %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/pages/tenants.html b/templates/pages/tenants.html index 5af7d67..7083e4e 100644 --- a/templates/pages/tenants.html +++ b/templates/pages/tenants.html @@ -9,13 +9,14 @@
- + {% if dashboard_names %} + {% for name in dashboard_names %} + + {% endfor %} + {% else %} + + {% endif %}
@@ -123,4 +124,4 @@ parentItem.after(nestedGroup); } -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/pages/users.html b/templates/pages/users.html index 63f73e7..ae7c5c2 100644 --- a/templates/pages/users.html +++ b/templates/pages/users.html @@ -9,11 +9,14 @@
- + {% if dashboard_names %} + {% for name in dashboard_names %} + + {% endfor %} + {% else %} + + {% endif %}
@@ -150,4 +153,4 @@ } }); -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/pages/vpn_status.html b/templates/pages/vpn_status.html index 8d6b102..1b659d0 100644 --- a/templates/pages/vpn_status.html +++ b/templates/pages/vpn_status.html @@ -9,11 +9,14 @@
- + {% if dashboard_names %} + {% for name in dashboard_names %} + + {% endfor %} + {% else %} + + {% endif %}
@@ -165,4 +168,4 @@ btn.innerHTML = 'Restart VPN'; }); -{% endblock %} \ No newline at end of file +{% endblock %}