Initial commit of AthonetTools
This commit is contained in:
90
Triton.conf
Normal file
90
Triton.conf
Normal file
@@ -0,0 +1,90 @@
|
||||
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>
|
||||
BIN
__pycache__/app.cpython-310.pyc
Normal file
BIN
__pycache__/app.cpython-310.pyc
Normal file
Binary file not shown.
BIN
__pycache__/auth_utils.cpython-310.pyc
Normal file
BIN
__pycache__/auth_utils.cpython-310.pyc
Normal file
Binary file not shown.
BIN
__pycache__/core_functions.cpython-310.pyc
Normal file
BIN
__pycache__/core_functions.cpython-310.pyc
Normal file
Binary file not shown.
BIN
ansible_workspace/network_tool_backup_08192025_1620.tar.gz
Normal file
BIN
ansible_workspace/network_tool_backup_08192025_1620.tar.gz
Normal file
Binary file not shown.
12
ansible_workspace/staging/host_vars/ESXI-1/esxi.yaml
Normal file
12
ansible_workspace/staging/host_vars/ESXI-1/esxi.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
vswitches:
|
||||
- vSwitchName: GAF_VSWITCH
|
||||
vSwitchNics: [vmnic4, vmnic5]
|
||||
vSwitchSecurity:
|
||||
forged_transmits: true
|
||||
mac_changes: true
|
||||
|
||||
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: 4095, vlanName: GAF_BP_T_510_515 }
|
||||
@@ -0,0 +1,68 @@
|
||||
# 3GPP core identifiers / names
|
||||
mcc: "315"
|
||||
mnc: "010"
|
||||
full_network_name: "JohnWayne"
|
||||
short_network_name: "JohnWayne"
|
||||
|
||||
# AMF / GUAMI
|
||||
amf_name: "amf01.5gc.3gppnetwork.org"
|
||||
guami:
|
||||
region: "02"
|
||||
set: "003"
|
||||
pointer: "000001"
|
||||
|
||||
# MME (for 4G interop / S1)
|
||||
mme_name: "mme1"
|
||||
mmegi: "0001"
|
||||
mmec: "01"
|
||||
mme_cname: "gw01.nodes"
|
||||
|
||||
# DNS info
|
||||
epc_dns_zone_data:
|
||||
# Additional PLMNs to handle
|
||||
plmns:
|
||||
- { mcc: '999', mnc: '99' }
|
||||
- { mcc: '001', mnc: '01' }
|
||||
- { mcc: '315', mnc: '010' }
|
||||
|
||||
# SBI configuration
|
||||
sbi:
|
||||
interface: lo
|
||||
base_address: 127.0.1.1/24
|
||||
|
||||
# Transports configuration
|
||||
_ngc_ext_aio_transport:
|
||||
|
||||
# AIO local transports
|
||||
- action: set_local_transports
|
||||
params: {}
|
||||
|
||||
# RAN transports (use RAN IP)
|
||||
- action: override_amf_n2_transport
|
||||
params: { address: 192.168.120.95, vrf: RAN }
|
||||
- action: override_mme_transport
|
||||
params: { s1_address: 192.168.120.95, s1_vrf: RAN }
|
||||
|
||||
# UPF transports (N3 on RAN)
|
||||
- action: override_upf_transport
|
||||
params:
|
||||
n3_interface: eth1
|
||||
n3_address: 192.168.120.95
|
||||
n3_vrf: RAN
|
||||
|
||||
# DN/DNN (N6) with UE pool
|
||||
- action: add_n6_dnn
|
||||
params:
|
||||
n6_dnn: internet
|
||||
n6_vrf: DN_01
|
||||
n6_vlan: 10
|
||||
n6_vrf_table: 511
|
||||
n6_interface: eth2
|
||||
n6_ip: 192.168.110.95/24
|
||||
n6_gw: 192.168.110.1
|
||||
n6_upf_pools:
|
||||
- upf_route: 100.0.94.0/24
|
||||
nssai: false
|
||||
n6_bgp:
|
||||
local_as: 65001
|
||||
peer_as: 65000
|
||||
@@ -0,0 +1,10 @@
|
||||
kind: ngcore-AIO
|
||||
nf_skip_list:
|
||||
- "aaa"
|
||||
- "chf"
|
||||
- "bmsc"
|
||||
- "dra"
|
||||
- "eir"
|
||||
version: '25.1'
|
||||
ova_file: /home/mjensen/OVA/HPE_ANW_P5G_Core-1.25.1.1-qemux86-64.ova
|
||||
report_services: false
|
||||
@@ -0,0 +1,33 @@
|
||||
# --- Networking recipe ---
|
||||
net_recipe: generic_bgp
|
||||
|
||||
# --- OAM config ---
|
||||
oam_network:
|
||||
add_ansible_host_address: false
|
||||
addresses:
|
||||
- 192.168.105.159/24
|
||||
gateway4: 192.168.105.1
|
||||
|
||||
# --- NTP ---
|
||||
ntp:
|
||||
- 0.pool.ntp.org
|
||||
- 1.pool.ntp.org
|
||||
|
||||
# --- VRF config ---
|
||||
_ngc_ext_aio_vrf:
|
||||
- action: net_add_vrf
|
||||
params: { name: RAN, table: 502 }
|
||||
- action: net_add_vrf
|
||||
params: { name: TELCO, table: 535 }
|
||||
|
||||
_ngc_ext_aio_net:
|
||||
# RAN interface
|
||||
- action: net_set_interface
|
||||
params:
|
||||
interface: eth1
|
||||
vrf: RAN
|
||||
addresses:
|
||||
- 192.168.120.95/24 # S1+N2+N3
|
||||
routes:
|
||||
- destination: 0.0.0.0/0
|
||||
gateway: 192.168.120.1
|
||||
@@ -0,0 +1,13 @@
|
||||
## UDM/UDR testing profile
|
||||
create_testing_profile:
|
||||
slices:
|
||||
- { sst: 1, sd: '000001' }
|
||||
- { sst: 1, sd: '' }
|
||||
dnns:
|
||||
- internet
|
||||
plmns:
|
||||
- { mcc: '315', mnc: '010' }
|
||||
|
||||
# UDR Subscribers provisioning
|
||||
udr_provisioning:
|
||||
testing_profile_service_profile: "testing_profile"
|
||||
19
ansible_workspace/staging/hosts.yaml
Normal file
19
ansible_workspace/staging/hosts.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
all:
|
||||
hosts:
|
||||
GBP08-AIO-1:
|
||||
ansible_host: 100.93.1.100
|
||||
children:
|
||||
ESXi:
|
||||
hosts:
|
||||
ESXI-1:
|
||||
VMs:
|
||||
children:
|
||||
_5GVMS:
|
||||
hosts:
|
||||
GBP08-AIO-1:
|
||||
_5GAIO:
|
||||
hosts:
|
||||
GBP08-AIO-1:
|
||||
vars:
|
||||
serialize: 2
|
||||
esxi_host: ESXI-1
|
||||
754
app.py
Normal file
754
app.py
Normal file
@@ -0,0 +1,754 @@
|
||||
from flask import Flask, render_template, request, jsonify, Response
|
||||
import core_functions
|
||||
import auth_utils
|
||||
import logging
|
||||
import os
|
||||
import urllib3; urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
import requests
|
||||
from requests.exceptions import HTTPError, RequestException
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
from services.state import set_target_ip, get_target_ip, set_mgmt_info, get_mgmt_info
|
||||
from services.combocore import login as cc_login, get_routes as cc_get_routes, extract_eth0_cidr_gw
|
||||
from services.local_net import get_eth0_dhcp_snapshot
|
||||
from services.remote_admin import (
|
||||
validate_ipv4,
|
||||
service_action,
|
||||
perform_service_sequence,
|
||||
)
|
||||
from services.yaml_writer import STAGING, render_to_file
|
||||
from pathlib import Path
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
app = Flask(__name__)
|
||||
|
||||
def _format_ipv6(host: str) -> str:
|
||||
return f"[{host}]" if ":" in host and not host.startswith("[") else host
|
||||
|
||||
def _fetch_eth0_from_remote(host: str) -> dict:
|
||||
fip = _format_ipv6(host)
|
||||
login_url = f"https://{fip}/core/pls/api/1/auth/login"
|
||||
routes_url = f"https://{fip}/core/ncm/api/1/status/routes"
|
||||
|
||||
# 1) get token
|
||||
r = requests.post(
|
||||
login_url,
|
||||
json={"username": "admin", "password": "Super4dmin!"},
|
||||
timeout=10, verify=False
|
||||
)
|
||||
r.raise_for_status()
|
||||
token = r.json().get("access_token")
|
||||
if not token:
|
||||
raise RuntimeError("No access_token in login response")
|
||||
|
||||
# 2) get routes
|
||||
r2 = requests.get(
|
||||
routes_url,
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=10, verify=False
|
||||
)
|
||||
r2.raise_for_status()
|
||||
routes = r2.json() # list of dicts
|
||||
|
||||
gw = None
|
||||
cidr = None
|
||||
# default route via eth0 -> gateway
|
||||
for item in routes:
|
||||
if item.get("family") == "inet" and item.get("dst") == "default" and item.get("dev") == "eth0":
|
||||
gw = item.get("gateway")
|
||||
break
|
||||
|
||||
# derive IP/ prefix from eth0 kernel link + subnet route
|
||||
prefsrc = None
|
||||
mask = None
|
||||
for item in routes:
|
||||
if item.get("family") == "inet" and item.get("dev") == "eth0" and item.get("type") == "unicast":
|
||||
if "prefsrc" in item and item["prefsrc"]:
|
||||
prefsrc = item["prefsrc"]
|
||||
if item.get("dst") and "/" in str(item["dst"]):
|
||||
mask = item["dst"].split("/", 1)[1]
|
||||
if prefsrc and mask:
|
||||
cidr = f"{prefsrc}/{mask}"
|
||||
|
||||
if not gw or not cidr:
|
||||
raise RuntimeError(f"Could not parse eth0 params gw={gw} cidr={cidr}")
|
||||
|
||||
return {"cidr": cidr, "gw": gw}
|
||||
|
||||
def fetch_oam_from_target(host: str) -> dict:
|
||||
"""
|
||||
Log in to the target 5GC and read the IPv4 OAM info from /core/ncm/api/1/status/routes.
|
||||
Returns dict like {"cidr":"192.168.86.54/24", "gw":"192.168.86.1"} or {"error": "..."}.
|
||||
"""
|
||||
base = f"https://{host}"
|
||||
login_url = f"{base}/core/pls/api/1/auth/login"
|
||||
routes_url = f"{base}/core/ncm/api/1/status/routes"
|
||||
|
||||
# --- Authenticate ---
|
||||
try:
|
||||
r = requests.post(
|
||||
login_url,
|
||||
json={"username": "admin", "password": "Super4dmin!"},
|
||||
timeout=10,
|
||||
verify=False,
|
||||
)
|
||||
r.raise_for_status()
|
||||
j = r.json()
|
||||
token = j.get("access_token") or j.get("token")
|
||||
if not token:
|
||||
return {"error": "login ok but no access_token in response"}
|
||||
except Exception as e:
|
||||
return {"error": f"login failed: {e}"}
|
||||
|
||||
render_to_file("aio_networking.yaml.j2", ctx, aio_networking_path)
|
||||
# --- Fetch routes ---
|
||||
try:
|
||||
r2 = requests.get(
|
||||
routes_url,
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=10,
|
||||
verify=False,
|
||||
)
|
||||
r2.raise_for_status()
|
||||
routes = r2.json() # expected: list of route dicts
|
||||
if not isinstance(routes, list):
|
||||
return {"error": "unexpected routes payload (not a list)"}
|
||||
except Exception as e:
|
||||
return {"error": f"routes fetch failed: {e}"}
|
||||
|
||||
# --- Parse gw (default v4 route) ---
|
||||
gw = None
|
||||
for rt in routes:
|
||||
if rt.get("family") == "inet" and rt.get("type") == "unicast" and rt.get("dst") == "default" and rt.get("dev") == "eth0":
|
||||
gw = rt.get("gateway")
|
||||
if gw:
|
||||
break
|
||||
|
||||
# --- Parse prefix length from the on-link route and combine with prefsrc ---
|
||||
ip_addr = None
|
||||
prefix_len = None
|
||||
for rt in routes:
|
||||
if rt.get("family") == "inet" and rt.get("dev") == "eth0":
|
||||
# kernel on-link route looks like 192.168.86.0/24 on eth0
|
||||
dst = rt.get("dst", "")
|
||||
if "/" in dst:
|
||||
try:
|
||||
prefix_len = int(dst.split("/", 1)[1])
|
||||
except Exception:
|
||||
pass
|
||||
# dhcp/kernel entries usually include prefsrc with the assigned IP
|
||||
ip_addr = ip_addr or rt.get("prefsrc")
|
||||
if ip_addr and prefix_len is not None:
|
||||
break
|
||||
|
||||
if not ip_addr or prefix_len is None:
|
||||
return {"error": "could not derive ip/prefix from routes"}
|
||||
|
||||
cidr = f"{ip_addr}/{prefix_len}"
|
||||
|
||||
# persist for later rendering
|
||||
try:
|
||||
from services.state import set_mgmt_info # avoid circular import issues
|
||||
set_mgmt_info(cidr, gw or "")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {"cidr": cidr, "gw": gw}
|
||||
|
||||
# --- Host target state (Stage 1) ---
|
||||
@app.post("/api/host/target")
|
||||
def api_set_target():
|
||||
ip = request.json.get("ip")
|
||||
try:
|
||||
ip = validate_ipv4(ip)
|
||||
set_target_ip(ip)
|
||||
return jsonify({"ok": True, "ip": ip})
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "error": str(e)}), 400
|
||||
|
||||
@app.post("/api/local/eth0/capture")
|
||||
def api_local_eth0_capture():
|
||||
data = request.json or {}
|
||||
host = data.get('host')
|
||||
try:
|
||||
# Use get_eth0_dhcp_snapshot or _fetch_eth0_from_remote depending on your setup
|
||||
if host:
|
||||
result = _fetch_eth0_from_remote(host)
|
||||
else:
|
||||
result = get_eth0_dhcp_snapshot()
|
||||
return jsonify({"ok": True, **result})
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "error": str(e)}), 500
|
||||
|
||||
@app.get("/api/local/eth0")
|
||||
def api_local_eth0():
|
||||
try:
|
||||
host = request.args.get("host") or get_target_ip()
|
||||
if not host:
|
||||
return jsonify({"ok": False, "error": "host not provided and no target set"}), 400
|
||||
snap = _fetch_eth0_from_remote(host)
|
||||
return jsonify({"ok": True, **snap})
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "error": str(e)}), 500
|
||||
|
||||
@app.get("/api/host/target")
|
||||
def api_get_target():
|
||||
return jsonify({"ip": get_target_ip()})
|
||||
|
||||
@app.get("/api/routes")
|
||||
def api_routes():
|
||||
rules = []
|
||||
for r in app.url_map.iter_rules():
|
||||
rules.append({
|
||||
"rule": str(r),
|
||||
"methods": sorted(m for m in r.methods if m not in {"HEAD","OPTIONS"}),
|
||||
"endpoint": r.endpoint,
|
||||
})
|
||||
return jsonify({"ok": True, "routes": rules})
|
||||
|
||||
@app.post("/api/host/capture_oam")
|
||||
def api_host_capture_oam():
|
||||
"""
|
||||
Use the saved target host (or JSON body 'host') to:
|
||||
1) login to ComboCore
|
||||
2) read routes
|
||||
3) extract eth0 cidr/gw
|
||||
4) persist via set_mgmt_info
|
||||
"""
|
||||
body = request.get_json(silent=True) or {}
|
||||
host = (body.get("host") or get_target_ip() or "").strip()
|
||||
if not host:
|
||||
return jsonify({"ok": False, "error": "No target host set. Set it via /api/host/target or include 'host' in body."}), 400
|
||||
|
||||
try:
|
||||
token = cc_login(host)
|
||||
routes = cc_get_routes(host, token)
|
||||
cidr, gw = extract_eth0_cidr_gw(routes)
|
||||
set_mgmt_info(cidr, gw)
|
||||
return jsonify({"ok": True, "host": host, "cidr": cidr, "gw": gw})
|
||||
except HTTPError as e:
|
||||
return jsonify({"ok": False, "host": host, "error": f"HTTP {e.response.status_code}: {e.response.text}"}), 502
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "host": host, "error": str(e)}), 500
|
||||
|
||||
# Enable SSH + Webconsole: enable → enable-autostart → start (serial)
|
||||
@app.post("/api/host/bootstrap_access")
|
||||
def api_bootstrap_access():
|
||||
ip = get_target_ip()
|
||||
if not ip:
|
||||
return jsonify({"ok": False, "error": "Target IP not set"}), 400
|
||||
try:
|
||||
perform_service_sequence(ip, "ssh", API_USER, API_PASS)
|
||||
perform_service_sequence(ip, "webconsole", API_USER, API_PASS)
|
||||
return jsonify({"ok": True})
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "error": str(e)}), 502
|
||||
|
||||
@app.get("/api/ping")
|
||||
def api_ping():
|
||||
return jsonify({"ok": True, "msg": "pong"})
|
||||
|
||||
def _ensure_dir(p: Path):
|
||||
p.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def _clean_dir(p: Path):
|
||||
if not p.exists():
|
||||
return
|
||||
for item in sorted(p.rglob("*"), reverse=True):
|
||||
if item.is_file():
|
||||
item.unlink()
|
||||
elif item.is_dir():
|
||||
try: item.rmdir()
|
||||
except OSError: pass
|
||||
|
||||
@app.post("/api/ansible/render")
|
||||
def api_ansible_render():
|
||||
body = request.get_json(force=True)
|
||||
|
||||
def _first(v): return (v or "").strip()
|
||||
|
||||
# --- Basic normalization ---
|
||||
plmn = _first(body.get("plmn"))
|
||||
mcc, mnc = (plmn.split("-")[0], plmn.split("-")[1]) if "-" in plmn else ("315", "010")
|
||||
|
||||
ran_cidr = _first(body.get("ran", {}).get("cidr"))
|
||||
ran_ip = ran_cidr.split("/")[0] if "/" in ran_cidr else ran_cidr
|
||||
vpn_ip = _first(body.get("ansible_host_ip")) or _first(get_target_ip())
|
||||
|
||||
mgmt_in = body.get("mgmt", {}) or {}
|
||||
mgmt_cidr = _first(mgmt_in.get("cidr"))
|
||||
mgmt_gw = _first(mgmt_in.get("gw"))
|
||||
# Always use values from payload, do not override with cached/remote values
|
||||
|
||||
print(f"[DEBUG] Rendering YAML with mgmt_cidr={mgmt_cidr}, mgmt_gw={mgmt_gw}")
|
||||
app.logger.info(f"[DEBUG] Rendering YAML with mgmt_cidr={mgmt_cidr}, mgmt_gw={mgmt_gw}")
|
||||
if not (mgmt_cidr and mgmt_gw):
|
||||
return jsonify({"ok": False, "error": "Cannot determine OAM (eth0) CIDR/gateway – run Stage 1 capture first."}), 400
|
||||
|
||||
inventory_host = _first(body.get("inventory_host") or "GBP08-AIO-1")
|
||||
esxi_host = _first(body.get("esxi_host") or "ESXI-1")
|
||||
|
||||
ctx = {
|
||||
"hostname": _first(body.get("hostname") or "AIO-1"),
|
||||
"network_name": _first(body.get("network_name") or "Network"),
|
||||
"plmn": plmn, "mcc": mcc, "mnc": mnc,
|
||||
"dns": body.get("dns", ["8.8.8.8"]),
|
||||
"ntp": body.get("ntp", ["0.pool.ntp.org", "1.pool.ntp.org"]),
|
||||
"ran": {"cidr": ran_cidr, "gw": _first(body.get("ran", {}).get("gw")), "ip": ran_ip},
|
||||
"mgmt": {
|
||||
"mode": "static",
|
||||
"cidr": mgmt_cidr,
|
||||
"gw": mgmt_gw,
|
||||
},
|
||||
"dn": {
|
||||
"cidr": _first(body.get("dn", {}).get("cidr")),
|
||||
"gw": _first(body.get("dn", {}).get("gw")),
|
||||
"vlan": body.get("dn", {}).get("vlan"),
|
||||
"ue_pool": _first(body.get("dn", {}).get("ue_pool")),
|
||||
"dnn": _first(body.get("dn", {}).get("dnn") or "internet"),
|
||||
},
|
||||
"inventory_host": inventory_host,
|
||||
"ansible_host_ip": vpn_ip or "127.0.0.1", # final fallback
|
||||
"esxi_host": esxi_host,
|
||||
"version": _first(body.get("version") or "25.1"),
|
||||
"ova_file": _first(body.get("ova_file") or "/home/mjensen/OVA/HPE_ANW_P5G_Core-1.25.1.1-qemux86-64.ova"),
|
||||
"report_services": bool(body.get("report_services", False)),
|
||||
}
|
||||
|
||||
# --- Write directly under staging/ (no scenario folder) ---
|
||||
base = STAGING
|
||||
|
||||
_clean_dir(base) # wipe staging each run (simple & predictable)
|
||||
|
||||
# Ensure folders exist
|
||||
aio_host_dir = base / "host_vars" / inventory_host
|
||||
esxi_host_dir = base / "host_vars" / esxi_host
|
||||
_ensure_dir(aio_host_dir)
|
||||
_ensure_dir(esxi_host_dir)
|
||||
|
||||
# --- Force re-render of YAML files with latest values ---
|
||||
render_to_file("hosts.yaml.j2", ctx, base / "hosts.yaml")
|
||||
render_to_file("aio_deploy.yaml.j2", ctx, aio_host_dir / "aio_deploy.yaml")
|
||||
# Always render aio_networking.yaml with correct context
|
||||
aio_networking_path = aio_host_dir / "aio_networking.yaml"
|
||||
print(f"[DEBUG] Rendering {aio_networking_path} with context: {ctx}")
|
||||
render_to_file("aio_networking.yaml.j2", ctx, aio_networking_path)
|
||||
render_to_file("aio_3gpp.yaml.j2", ctx, aio_host_dir / "aio_3gpp.yaml")
|
||||
render_to_file("aio_provisioning.yaml.j2", ctx, aio_host_dir / "aio_provisioning.yaml")
|
||||
render_to_file("esxi.yaml.j2", ctx, esxi_host_dir / "esxi.yaml")
|
||||
|
||||
# Always return success if no exception occurred
|
||||
return jsonify({"ok": True, "staging": str(base)})
|
||||
|
||||
@app.post("/api/host/service/<service>/<action>")
|
||||
def api_service_action(service, action):
|
||||
service = service.lower()
|
||||
action = action.lower()
|
||||
ip = get_target_ip()
|
||||
if not ip:
|
||||
return jsonify({"ok": False, "error": "Target IP not set"}), 400
|
||||
try:
|
||||
# optional early guard (mirrors remote_admin.ALLOWED_*):
|
||||
if service not in {"ssh","webconsole"} or action not in {"enable","enable-autostart","start"}:
|
||||
return jsonify({"ok": False, "error": "Unsupported service/action"}), 400
|
||||
|
||||
service_action(ip, service, action, API_USER, API_PASS)
|
||||
return jsonify({"ok": True})
|
||||
except Exception as e:
|
||||
app.logger.exception("service_action failed")
|
||||
return jsonify({"ok": False, "error": str(e)}), 502
|
||||
|
||||
|
||||
|
||||
|
||||
# --- Page Routes ---
|
||||
|
||||
@app.route("/")
|
||||
def vpn_status_page():
|
||||
return render_template("pages/vpn_status.html", active_page='vpn_status')
|
||||
|
||||
@app.route("/network-config")
|
||||
def network_config_page():
|
||||
return render_template("pages/network_config.html", active_page='network_config')
|
||||
|
||||
@app.route("/tenants")
|
||||
def tenants_page():
|
||||
return render_template("pages/tenants.html", active_page='tenants')
|
||||
|
||||
@app.route("/hnk")
|
||||
def hnk_page():
|
||||
return render_template("pages/hnk.html", active_page='hnk')
|
||||
|
||||
@app.route("/network-clients")
|
||||
def network_clients_page():
|
||||
return render_template("pages/network_clients.html", active_page='network_clients')
|
||||
|
||||
@app.route("/system-browser")
|
||||
def system_browser_page():
|
||||
return render_template("pages/system_browser.html", active_page='system_browser')
|
||||
|
||||
@app.route("/users")
|
||||
def users_page():
|
||||
return render_template("pages/users.html", active_page='users')
|
||||
|
||||
@app.route("/m2000-reset")
|
||||
def m2000_reset_page():
|
||||
return render_template("pages/m2000_config_reset.html", active_page='m2000_reset')
|
||||
|
||||
@app.route("/api/ansible/deploy", methods=["POST"])
|
||||
def api_ansible_deploy():
|
||||
import subprocess
|
||||
staging_dir = "/home/mjensen/network_tool/ansible_workspace/staging"
|
||||
try:
|
||||
import subprocess
|
||||
outputs = []
|
||||
try:
|
||||
import os
|
||||
# Ensure PATH includes system binaries
|
||||
os.environ["PATH"] = os.environ.get("PATH", "") + ":/usr/bin:/usr/local/bin:/bin"
|
||||
env_info = f"USER: {os.environ.get('USER')}\nPATH: {os.environ.get('PATH')}\n"
|
||||
result_docker = subprocess.run(['/usr/bin/docker', 'ps'], capture_output=True, text=True)
|
||||
docker_output = result_docker.stdout + result_docker.stderr
|
||||
result_script = subprocess.run(
|
||||
'/usr/bin/script -q -c "/usr/local/bin/ath-gaf-cli --ova-path /OVA" /dev/null <<< "playbook-ngc-config"',
|
||||
cwd=staging_dir,
|
||||
shell=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
executable='/bin/bash'
|
||||
)
|
||||
output = env_info + docker_output + result_script.stdout + result_script.stderr
|
||||
# Return as plain text if not valid JSON
|
||||
from flask import Response
|
||||
return Response(output, mimetype='text/plain')
|
||||
except Exception as e:
|
||||
return jsonify({"output": f"Error: {str(e)}"}), 500
|
||||
except Exception as e:
|
||||
return jsonify({"output": f"Error: {str(e)}"}), 500
|
||||
|
||||
@app.route("/vpn-switcher")
|
||||
def vpn_switcher_page():
|
||||
ip_from_url = request.args.get('ip', None)
|
||||
return render_template("pages/vpn_switcher.html", active_page='vpn_switcher', ip_from_url=ip_from_url)
|
||||
|
||||
@app.route("/m2000psw")
|
||||
def m2000_password_page():
|
||||
serial_from_url = request.args.get('serial', None)
|
||||
return render_template("pages/m2000_password.html", active_page='m2000_password', serial_from_url=serial_from_url)
|
||||
|
||||
@app.route("/gaf-desk")
|
||||
def gaf_desk_page():
|
||||
return render_template("pages/gaf_desk.html", active_page='gaf_desk')
|
||||
|
||||
|
||||
# --- API Page Routes ---
|
||||
|
||||
@app.route("/api/m2000/list", methods=["POST"])
|
||||
def api_list_m2000():
|
||||
data = request.json
|
||||
dashboard_name = data.get('dashboard')
|
||||
base_url = DASHBOARD_URLS.get(dashboard_name)
|
||||
try:
|
||||
token, session = auth_utils.get_vpn_dashboard_token(base_url)
|
||||
devices = core_functions.list_m2000_vpns(base_url, token, session)
|
||||
return jsonify(devices)
|
||||
except Exception as e:
|
||||
app.logger.error(f"Error in /api/m2000/list: {e}", exc_info=True)
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route("/api/network/get-config", methods=["POST"])
|
||||
def api_get_network_config():
|
||||
data = request.json
|
||||
dashboard_name = data.get('dashboard')
|
||||
base_url = DASHBOARD_URLS.get(dashboard_name)
|
||||
try:
|
||||
token, session = auth_utils.get_vpn_dashboard_token(base_url)
|
||||
config_data = core_functions.get_full_network_config(base_url, token, session)
|
||||
return jsonify(config_data)
|
||||
except Exception as e:
|
||||
app.logger.error(f"Error in /api/network/get-config: {e}", exc_info=True)
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route("/api/tenants/list", methods=["POST"])
|
||||
def api_list_tenants():
|
||||
data = request.json
|
||||
dashboard_name = data.get('dashboard')
|
||||
base_url = DASHBOARD_URLS.get(dashboard_name)
|
||||
try:
|
||||
token, session = auth_utils.get_vpn_dashboard_token(base_url)
|
||||
tenants = core_functions.list_tenants(base_url, token, session)
|
||||
return jsonify(tenants)
|
||||
except Exception as e:
|
||||
app.logger.error(f"Error listing tenants: {e}", exc_info=True)
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route("/api/plmns/list", methods=["POST"])
|
||||
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:
|
||||
token, session = auth_utils.get_vpn_dashboard_token(base_url)
|
||||
plmns = core_functions.list_plmns(base_url, token, session, tenant_id)
|
||||
return jsonify(plmns)
|
||||
except Exception as e:
|
||||
app.logger.error(f"Error listing PLMNs for tenant {tenant_id}: {e}", exc_info=True)
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/generate_yaml', methods=['POST'])
|
||||
def generate_yaml():
|
||||
data = request.json
|
||||
# Use Jinja2 to render from .j2 templates
|
||||
# Example:
|
||||
rendered = render_template('my_template.j2', **data)
|
||||
output_path = f"/path/to/output/{data['network_name']}.yaml"
|
||||
with open(output_path, 'w') as f:
|
||||
f.write(rendered)
|
||||
return jsonify({"status": "success", "file": output_path})
|
||||
|
||||
@app.route("/api/vpn/get-config", methods=["POST"])
|
||||
def api_get_vpn_config():
|
||||
data = request.json
|
||||
host_ip = data.get('host_ip')
|
||||
if not host_ip:
|
||||
return jsonify({"error": "Host IP is missing"}), 400
|
||||
try:
|
||||
combined_data = core_functions.get_vpn_config_and_details(host_ip)
|
||||
return jsonify(combined_data)
|
||||
except Exception as e:
|
||||
app.logger.error(f"Error in VPN switcher for {host_ip}: {e}", exc_info=True)
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route("/api/vpn/get-endpoint", methods=["POST"])
|
||||
def api_get_vpn_endpoint():
|
||||
data = request.json
|
||||
host_ip = data.get('host_ip')
|
||||
if not host_ip:
|
||||
return jsonify({"error": "Host IP is missing"}), 400
|
||||
try:
|
||||
endpoint_info = core_functions.get_current_vpn_endpoint(host_ip)
|
||||
return jsonify(endpoint_info)
|
||||
except Exception as e:
|
||||
app.logger.error(f"Error getting VPN endpoint for {host_ip}: {e}", exc_info=True)
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route("/api/vpn/set-endpoint", methods=["POST"])
|
||||
def api_set_vpn_endpoint():
|
||||
data = request.json
|
||||
host_ip = data.get('host_ip')
|
||||
region = data.get('region')
|
||||
if not all([host_ip, region]):
|
||||
return jsonify({"error": "Missing required parameters"}), 400
|
||||
try:
|
||||
result = core_functions.set_vpn_endpoint(host_ip, region)
|
||||
return jsonify(result)
|
||||
except Exception as e:
|
||||
app.logger.error(f"Error setting VPN endpoint for {host_ip}: {e}", exc_info=True)
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route("/api/hnks/list/by-plmn", methods=["POST"])
|
||||
def api_list_plmn_hnks():
|
||||
data = request.json
|
||||
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:
|
||||
token, session = auth_utils.get_vpn_dashboard_token(base_url)
|
||||
hnks = core_functions.list_plmn_hnks(base_url, token, session, tenant_id, plmn_id)
|
||||
return jsonify(hnks)
|
||||
except Exception as e:
|
||||
app.logger.error(f"Error listing HNKs for PLMN {plmn_id}: {e}", exc_info=True)
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route("/api/m2000/get-password", methods=["POST"])
|
||||
def api_get_m2000_password():
|
||||
data = request.json
|
||||
serial = data.get('serial')
|
||||
if not serial:
|
||||
return jsonify({"error": "Serial number is missing"}), 400
|
||||
try:
|
||||
password = core_functions.generate_m2000_password(serial)
|
||||
return jsonify({"serial": serial, "password": password})
|
||||
except Exception as e:
|
||||
app.logger.error(f"Error generating password for serial {serial}: {e}", exc_info=True)
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route("/api/m2000/reset-config", methods=["POST"])
|
||||
def api_m2000_reset_config():
|
||||
data = request.json
|
||||
base_ip = data.get('base_ip')
|
||||
if not base_ip:
|
||||
return jsonify({"error": "Base IPv6 address is missing"}), 400
|
||||
try:
|
||||
reset_results = core_functions.reset_m2000_configuration(base_ip)
|
||||
return jsonify(reset_results)
|
||||
except Exception as e:
|
||||
app.logger.error(f"Error in m2000 config reset for base IP {base_ip}: {e}", exc_info=True)
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route("/api/hnk/list", methods=["POST"])
|
||||
def api_list_hnk():
|
||||
data = request.json
|
||||
host_ip = data.get('host')
|
||||
if not host_ip:
|
||||
return jsonify({"error": "Host IP is missing"}), 400
|
||||
try:
|
||||
token = auth_utils.authenticate(host_ip)
|
||||
hnk_data = core_functions.list_home_network_keys(host_ip, token)
|
||||
return jsonify(hnk_data)
|
||||
except Exception as e:
|
||||
app.logger.error(f"Error in /api/hnk/list: {e}", exc_info=True)
|
||||
return jsonify({"error": "An internal error occurred. Check server logs."}), 500
|
||||
|
||||
@app.route("/host/<host_ip>")
|
||||
def host_details_page(host_ip):
|
||||
details_from_browser = {
|
||||
"customer_name": request.args.get('customer_name', 'N/A'),
|
||||
"common_name": request.args.get('common_name', 'N/A'),
|
||||
"public_ip": request.args.get('public_ip', 'N/A'),
|
||||
"connected_since": request.args.get('connected_since', 'N/A')
|
||||
}
|
||||
|
||||
try:
|
||||
# Fetch the live, detailed information from the host
|
||||
live_details = core_functions.get_host_details(host_ip)
|
||||
|
||||
# Combine both sets of data to pass to the template
|
||||
live_details['browser_info'] = details_from_browser
|
||||
|
||||
return render_template("pages/host_details.html", details=live_details)
|
||||
except Exception as e:
|
||||
app.logger.error(f"Error getting details for host {host_ip}: {e}", exc_info=True)
|
||||
# Pass the browser info even if the live fetch fails
|
||||
error_details = {"browser_info": details_from_browser, "error": True}
|
||||
return render_template("pages/host_details.html", details=error_details)
|
||||
|
||||
@app.route("/api/m2000/restart", methods=["POST"])
|
||||
def api_restart_m2000():
|
||||
data = request.json
|
||||
serial = data.get('serial')
|
||||
subnet = data.get('subnet')
|
||||
try:
|
||||
result = core_functions.restart_m2000_vpn(serial, subnet)
|
||||
return jsonify(result)
|
||||
except Exception as e:
|
||||
app.logger.error(f"An exception occurred during VPN restart for serial {serial}:", exc_info=True)
|
||||
return jsonify({"error": "An internal error occurred. Check server logs."}), 500
|
||||
|
||||
@app.route("/api/vpn/status", methods=["GET"])
|
||||
def api_vpn_status():
|
||||
try:
|
||||
active_vpn = core_functions.get_active_vpn()
|
||||
return jsonify({"active_vpn": active_vpn})
|
||||
except Exception as e:
|
||||
app.logger.error(f"Error getting VPN status: {e}", exc_info=True)
|
||||
return jsonify({"error": "Failed to get VPN status"}), 500
|
||||
|
||||
@app.route("/api/vpn/toggle", methods=["POST"])
|
||||
def api_vpn_toggle():
|
||||
data = request.json
|
||||
vpn_name = data.get('vpn_name')
|
||||
turn_on = data.get('state', False)
|
||||
try:
|
||||
new_active_vpn = core_functions.toggle_vpn_connection(vpn_name, turn_on)
|
||||
return jsonify({"status": "success", "active_vpn": new_active_vpn})
|
||||
except Exception as e:
|
||||
app.logger.error(f"Error toggling VPN {vpn_name}: {e}", exc_info=True)
|
||||
return jsonify({"error": f"Failed to toggle VPN {vpn_name}"}), 500
|
||||
|
||||
@app.route("/api/network/update-radios", methods=["POST"])
|
||||
def api_update_radios():
|
||||
data = request.json
|
||||
dashboard_name = data.get('dashboard')
|
||||
network_id = data.get('network_id')
|
||||
new_count = data.get('new_count')
|
||||
operation = data.get('operation')
|
||||
base_url = DASHBOARD_URLS.get(dashboard_name)
|
||||
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)
|
||||
return jsonify({"status": "success", "message": "Radio count updated successfully.", "details": result})
|
||||
except Exception as e:
|
||||
app.logger.error(f"Error updating radio count for network {network_id}: {e}", exc_info=True)
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route("/api/system-browser/data", methods=["POST"])
|
||||
def api_get_system_browser_data():
|
||||
try:
|
||||
browser_data = core_functions.get_system_browser_data()
|
||||
return jsonify(browser_data)
|
||||
except Exception as e:
|
||||
app.logger.error(f"Error getting system browser data: {e}", exc_info=True)
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route("/api/backup/create", methods=["POST"])
|
||||
def api_create_backup():
|
||||
data = request.json
|
||||
host_ip = data.get('host')
|
||||
if not host_ip:
|
||||
return Response("Host IP is missing", status=400)
|
||||
|
||||
try:
|
||||
token = auth_utils.authenticate(host_ip)
|
||||
backup_response = core_functions.create_backup(host_ip, token)
|
||||
|
||||
return Response(
|
||||
backup_response.iter_content(chunk_size=1024),
|
||||
content_type=backup_response.headers.get('Content-Type'),
|
||||
headers={"Content-Disposition": backup_response.headers.get('Content-Disposition')}
|
||||
)
|
||||
except Exception as e:
|
||||
app.logger.error(f"Error creating backup for host {host_ip}: {e}", exc_info=True)
|
||||
return Response(f"An error occurred on the server: {e}", status=500)
|
||||
|
||||
@app.route("/api/host/details", methods=["POST"])
|
||||
def api_get_host_details():
|
||||
data = request.json
|
||||
host_ip = data.get('host')
|
||||
if not host_ip:
|
||||
return jsonify({"error": "Host IP is missing"}), 400
|
||||
|
||||
try:
|
||||
token = auth_utils.authenticate(host_ip)
|
||||
system_info = core_functions.get_system_info(host_ip, token)
|
||||
site_info = core_functions.get_site_info(host_ip, token)
|
||||
frontend_config = core_functions.get_frontend_config(host_ip, token)
|
||||
|
||||
# Combine all the data into a single response
|
||||
combined_data = {
|
||||
"system": system_info,
|
||||
"site": site_info,
|
||||
"services": frontend_config.get("services", [])
|
||||
}
|
||||
return jsonify(combined_data)
|
||||
except Exception as e:
|
||||
app.logger.error(f"Error getting host details for {host_ip}: {e}", exc_info=True)
|
||||
return jsonify({"error": "An internal error occurred"}), 500
|
||||
|
||||
@app.route("/api/users/list", methods=["POST"])
|
||||
def api_list_users():
|
||||
data = request.json
|
||||
dashboard_name = data.get('dashboard')
|
||||
base_url = DASHBOARD_URLS.get(dashboard_name)
|
||||
try:
|
||||
token, session = auth_utils.get_vpn_dashboard_token(base_url)
|
||||
users = core_functions.list_users(base_url, token, session)
|
||||
return jsonify(users)
|
||||
except Exception as e:
|
||||
app.logger.error(f"Error listing users: {e}", exc_info=True)
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(debug=True)
|
||||
58
auth_utils.py
Normal file
58
auth_utils.py
Normal file
@@ -0,0 +1,58 @@
|
||||
import requests
|
||||
import json
|
||||
from requests.exceptions import HTTPError
|
||||
|
||||
requests.packages.urllib3.disable_warnings()
|
||||
|
||||
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("["):
|
||||
return f"[{host_ip}]"
|
||||
return host_ip
|
||||
|
||||
# ----- ComboCore --------
|
||||
|
||||
def authenticate(host_ip):
|
||||
formatted_ip = _format_ipv6(host_ip) # Use the helper
|
||||
url = f"https://{formatted_ip}/core/pls/api/1/auth/login"
|
||||
payload = {"username": "admin", "password": "Super4dmin!"}
|
||||
|
||||
try:
|
||||
response = requests.post(url, json=payload, verify=False)
|
||||
response.raise_for_status()
|
||||
|
||||
return response.json()["access_token"]
|
||||
|
||||
except HTTPError as http_err:
|
||||
raise http_err
|
||||
|
||||
# ----- Dashboard --------
|
||||
|
||||
def get_vpn_dashboard_token(base_url):
|
||||
session = requests.Session()
|
||||
session.headers.update({
|
||||
"Content-Type": "application/json", "Accept": "*/*", "User-Agent": "Mozilla/5.0"
|
||||
})
|
||||
|
||||
credentials = {
|
||||
"user": "admin@hpe.com", "password": "JohnWayne#21",
|
||||
# "user": "admin@athonet.com", "password": "administratoR!1",
|
||||
"lang": "en", "auth_provider": "enterprise"
|
||||
}
|
||||
|
||||
auth_response = session.post(f"{base_url}/portal/api/session/authenticate", json=credentials, verify=False)
|
||||
auth_response.raise_for_status()
|
||||
tenant_id = auth_response.json().get("tenants", [{}])[0].get("id")
|
||||
|
||||
if not tenant_id:
|
||||
raise ValueError("Authentication failed: Could not retrieve Tenant ID.")
|
||||
|
||||
login_data = {"tenant_id": tenant_id, **credentials}
|
||||
login_response = session.post(f"{base_url}/portal/api/session/login", json=login_data, verify=False)
|
||||
login_response.raise_for_status()
|
||||
token = login_response.json().get("token")
|
||||
|
||||
if not token:
|
||||
raise ValueError("Login failed: Could not retrieve session token.")
|
||||
|
||||
return token, session
|
||||
535
core_functions.py
Normal file
535
core_functions.py
Normal file
@@ -0,0 +1,535 @@
|
||||
import requests
|
||||
import json
|
||||
import paramiko
|
||||
import subprocess
|
||||
import time
|
||||
import re
|
||||
import os
|
||||
import auth_utils
|
||||
import hashlib
|
||||
from requests.exceptions import HTTPError
|
||||
from datetime import datetime
|
||||
|
||||
requests.packages.urllib3.disable_warnings()
|
||||
|
||||
VPN_CONFIG_NAMES = ["Triton", "Star", "Bluebonnet", "Lonestar", "Production", "US-Support", "EU-Support"]
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
def list_home_network_keys(host_ip, token):
|
||||
url = f"https://{host_ip}/core/udm/api/1/provisioning/home_network_keys"
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
response = requests.get(url, headers=headers, verify=False)
|
||||
response.raise_for_status()
|
||||
return response.json().get("data", [])
|
||||
|
||||
def get_home_network_key(host_ip, token, key_id):
|
||||
url = f"https://{host_ip}/core/udm/api/1/provisioning/home_network_keys/{key_id}"
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
response = requests.get(url, headers=headers, verify=False)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
def create_home_network_key(host_ip, token, key_id, home_network_identifier, private_key, profile, description=None):
|
||||
url = f"https://{host_ip}/core/udm/api/1/provisioning/home_network_keys"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
payload = {
|
||||
"key_id": key_id,
|
||||
"home_network_identifier": home_network_identifier,
|
||||
"private_key": private_key,
|
||||
"profile": profile
|
||||
}
|
||||
if description:
|
||||
payload["description"] = description
|
||||
|
||||
response = requests.post(url, headers=headers, json=payload, verify=False)
|
||||
response.raise_for_status()
|
||||
return {"status": "success", "message": f"Home Network Key with ID {key_id} created successfully."}
|
||||
|
||||
def delete_home_network_key(host_ip, token, key_id):
|
||||
"""Deletes a Home Network Key and returns a success message."""
|
||||
url = f"https://{host_ip}/core/udm/api/1/provisioning/home_network_keys/{key_id}"
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
response = requests.delete(url, headers=headers, verify=False)
|
||||
response.raise_for_status()
|
||||
return {"status": "success", "message": f"Home Network Key with ID {key_id} deleted successfully."}
|
||||
|
||||
def list_m2000_vpns(base_url, token, session):
|
||||
"""Lists all network devices from the Aruba dashboard."""
|
||||
network_url = f"{base_url}/portal/api/1/network"
|
||||
auth_headers = session.headers.copy()
|
||||
auth_headers["horus-token"] = token
|
||||
|
||||
response = session.get(network_url, headers=auth_headers, verify=False)
|
||||
response.raise_for_status()
|
||||
|
||||
processed_items = []
|
||||
items = response.json().get("items", [])
|
||||
for item in items:
|
||||
hw_list = item.get("info", {}).get("hardware", [])
|
||||
for hw in hw_list:
|
||||
processed_items.append({
|
||||
"id": item.get("id"),
|
||||
"name": item.get("name"),
|
||||
"status": item.get("status"),
|
||||
"serial": hw.get("serial"),
|
||||
"subnet": hw.get("subnet_delegation")
|
||||
})
|
||||
return processed_items
|
||||
|
||||
def restart_m2000_vpn(serial, subnet):
|
||||
import ipaddress
|
||||
try:
|
||||
password = SERIAL_PASSWORDS.get(serial)
|
||||
if not password:
|
||||
raise ValueError(f"No password found for serial {serial}")
|
||||
|
||||
subnet_obj = ipaddress.ip_network(subnet, strict=False)
|
||||
router_ip = str(list(subnet_obj.hosts())[0])
|
||||
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect(router_ip, username='root', password=password, timeout=10)
|
||||
|
||||
ssh.exec_command('systemctl restart openvpn@openvpn.service')
|
||||
|
||||
ssh.close()
|
||||
|
||||
return {"status": "success", "message": f"Restart command sent to {serial}. Please refresh the list in a moment to see the updated status."}
|
||||
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
def get_vpn_config_and_details(host_ip):
|
||||
"""
|
||||
Connects to a host via SSH, gets VPN config, and fetches system details.
|
||||
"""
|
||||
# --- 1. Get VPN Config and Current Endpoint via SSH ---
|
||||
key_path = os.path.expanduser("~/.ssh/5G-SSH-Key.pem")
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect(host_ip, username='root', key_filename=key_path, timeout=10)
|
||||
|
||||
stdin, stdout, stderr = ssh.exec_command('head -n 5 /etc/openvpn/client/athonet.conf')
|
||||
vpn_config_output = stdout.read().decode().strip()
|
||||
|
||||
# Also get the current endpoint IP from the config
|
||||
stdin, stdout, stderr = ssh.exec_command("grep '^remote' /etc/openvpn/client/athonet.conf")
|
||||
config_line = stdout.read().decode().strip()
|
||||
ssh.close()
|
||||
|
||||
if not config_line:
|
||||
raise Exception("Could not read VPN configuration from host.")
|
||||
|
||||
current_ip = config_line.split()[1]
|
||||
current_region = "Unknown"
|
||||
for region, ip in VPN_ENDPOINTS.items():
|
||||
if ip == current_ip:
|
||||
current_region = region
|
||||
break
|
||||
|
||||
# --- 2. Get Host Details via API ---
|
||||
host_details = get_host_details(host_ip)
|
||||
|
||||
# --- 3. Combine all the results ---
|
||||
return {
|
||||
"vpn_config": vpn_config_output,
|
||||
"vpn_endpoint": {"region": current_region, "ip": current_ip},
|
||||
"details": host_details
|
||||
}
|
||||
|
||||
VPN_ENDPOINTS = {
|
||||
"US": "128.136.82.165",
|
||||
"EU": "156.54.30.27"
|
||||
}
|
||||
|
||||
def get_current_vpn_endpoint(host_ip):
|
||||
"""Connects to a host and reads the current VPN endpoint from the config file."""
|
||||
key_path = os.path.expanduser("~/.ssh/5G-SSH-Key.pem")
|
||||
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect(host_ip, username='root', key_filename=key_path, timeout=10)
|
||||
|
||||
# Read the 'remote' line from the config file
|
||||
stdin, stdout, stderr = ssh.exec_command("grep '^remote' /etc/openvpn/client/athonet.conf")
|
||||
config_line = stdout.read().decode().strip()
|
||||
ssh.close()
|
||||
|
||||
if not config_line:
|
||||
raise Exception("Could not read VPN configuration from host.")
|
||||
|
||||
current_ip = config_line.split()[1]
|
||||
|
||||
# Determine if it's US or EU
|
||||
for region, ip in VPN_ENDPOINTS.items():
|
||||
if ip == current_ip:
|
||||
return {"region": region, "ip": ip}
|
||||
|
||||
return {"region": "Unknown", "ip": current_ip}
|
||||
|
||||
def set_vpn_endpoint(host_ip, region):
|
||||
"""Connects to a host, updates the VPN endpoint, and restarts the service."""
|
||||
if region not in VPN_ENDPOINTS:
|
||||
raise ValueError("Invalid region specified.")
|
||||
|
||||
new_ip = VPN_ENDPOINTS[region]
|
||||
key_path = os.path.expanduser("~/.ssh/5G-SSH-Key.pem")
|
||||
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect(host_ip, username='root', key_filename=key_path, timeout=10)
|
||||
|
||||
# Use sed to replace the IP in the config file and then restart the service
|
||||
command = (
|
||||
f"sed -i 's/^remote .*/remote {new_ip}/' /etc/openvpn/client/athonet.conf && "
|
||||
"systemctl restart openvpn-client@athonet.service"
|
||||
)
|
||||
stdin, stdout, stderr = ssh.exec_command(command)
|
||||
|
||||
# It's good practice to check for errors
|
||||
error = stderr.read().decode().strip()
|
||||
ssh.close()
|
||||
|
||||
if error:
|
||||
raise Exception(f"Failed to switch VPN: {error}")
|
||||
|
||||
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
|
||||
|
||||
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"])
|
||||
|
||||
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.")
|
||||
|
||||
return get_active_vpn()
|
||||
|
||||
def get_full_network_config(base_url, token, session):
|
||||
network_url = f"{base_url}/portal/api/1/network"
|
||||
auth_headers = session.headers.copy()
|
||||
auth_headers["horus-token"] = token
|
||||
response = session.get(network_url, headers=auth_headers, verify=False)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
def list_tenants(base_url, token, session):
|
||||
url = f"{base_url}/portal/api/tenants/"
|
||||
headers = session.headers.copy()
|
||||
headers["horus-token"] = token
|
||||
response = session.get(url, headers=headers, verify=False)
|
||||
response.raise_for_status()
|
||||
return response.json().get("tenants", [])
|
||||
|
||||
def list_plmns(base_url, token, session, tenant_id):
|
||||
url = f"{base_url}/portal/api/tenants/{tenant_id}/plmns"
|
||||
headers = session.headers.copy()
|
||||
headers["horus-token"] = token
|
||||
response = session.get(url, headers=headers, verify=False)
|
||||
response.raise_for_status()
|
||||
return response.json().get("items", [])
|
||||
|
||||
def list_plmn_hnks(base_url, token, session, tenant_id, plmn_id):
|
||||
url = f"{base_url}/portal/api/tenants/{tenant_id}/plmns/{plmn_id}/home-network-keys"
|
||||
headers = session.headers.copy()
|
||||
headers["horus-token"] = token
|
||||
response = session.get(url, headers=headers, verify=False)
|
||||
response.raise_for_status()
|
||||
return response.json().get("items", [])
|
||||
|
||||
def update_radio_count(base_url, token, session, network_id, new_count, operation):
|
||||
url = f"{base_url}/portal/api/1/network/{network_id}"
|
||||
headers = {
|
||||
"horus-token": token,
|
||||
"Content-Type": "application/json-patch+json"
|
||||
}
|
||||
payload = {"ops": []}
|
||||
if operation == 'replace':
|
||||
op_details = {
|
||||
"op": "replace",
|
||||
"path": "/info/radio_pool/0/num_of_radios",
|
||||
"value": str(new_count)
|
||||
}
|
||||
else:
|
||||
op_details = {
|
||||
"op": "add",
|
||||
"path": "/info/radio_pool",
|
||||
"value": [{"num_of_radios": str(new_count)}]
|
||||
}
|
||||
payload["ops"].append(op_details)
|
||||
response = session.patch(url, headers=headers, json=payload, verify=False)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
def get_system_browser_data():
|
||||
customers = {}
|
||||
try:
|
||||
with open('customers.txt', 'r') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line:
|
||||
parts = line.split(',', 1)
|
||||
if len(parts) == 2:
|
||||
customers[parts[0]] = parts[1]
|
||||
except FileNotFoundError:
|
||||
raise Exception("Error: customers.txt file not found on the server.")
|
||||
|
||||
vpn_clients = {}
|
||||
routing_table = {}
|
||||
vpn_status_urls = [
|
||||
"http://100.127.0.1/_vpn_status/t2-status.txt",
|
||||
"http://100.127.0.6/_vpn_status/t2-status.txt"
|
||||
]
|
||||
|
||||
for url in vpn_status_urls:
|
||||
try:
|
||||
response = requests.get(url, timeout=10)
|
||||
response.raise_for_status()
|
||||
|
||||
lines = response.text.split('\n')
|
||||
is_parsing_routing_table = False
|
||||
for line in lines:
|
||||
if "ROUTING TABLE" in line or "ROUTING_TABLE" in line:
|
||||
is_parsing_routing_table = True
|
||||
continue
|
||||
if not line.strip() or line.startswith(("TITLE", "TIME", "HEADER", "GLOBAL", "OpenVPN", "Updated", "END")):
|
||||
continue
|
||||
|
||||
parts = line.split(',')
|
||||
common_name = ""
|
||||
real_address = ""
|
||||
virtual_ip = "N/A"
|
||||
connected_since = ""
|
||||
|
||||
if is_parsing_routing_table:
|
||||
if len(parts) >= 2:
|
||||
routing_table[parts[1]] = parts[0]
|
||||
elif line.startswith("CLIENT_LIST"):
|
||||
if len(parts) > 7:
|
||||
common_name, real_address, virtual_ip, connected_since = parts[1], parts[2], parts[3] if parts[3] else "N/A", parts[7]
|
||||
elif len(parts) >= 5:
|
||||
common_name, real_address, connected_since = parts[0], parts[1], parts[4]
|
||||
|
||||
if common_name and common_name not in vpn_clients:
|
||||
customer_id_match = re.search(r'(\d{3})z', common_name)
|
||||
customer_id = customer_id_match.group(1) if customer_id_match else "N/A"
|
||||
customer_name = customers.get(customer_id, "Unknown")
|
||||
|
||||
# Clean the public IP to remove the port
|
||||
public_ip = real_address.split(':')[0]
|
||||
|
||||
vpn_clients[common_name] = {
|
||||
"customer_id": customer_id, "customer_name": customer_name,
|
||||
"common_name": common_name, "virtual_ip": virtual_ip,
|
||||
"public_ip": public_ip, "connected_since": connected_since
|
||||
}
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Warning: Could not fetch VPN status from {url}. Error: {e}")
|
||||
continue
|
||||
|
||||
for name, client_data in vpn_clients.items():
|
||||
if client_data["virtual_ip"] == "N/A" and name in routing_table:
|
||||
client_data["virtual_ip"] = routing_table[name]
|
||||
|
||||
if not vpn_clients:
|
||||
raise Exception("Connection failed. Please ensure you are connected to the correct VPN.")
|
||||
|
||||
return list(vpn_clients.values())
|
||||
|
||||
# ------- System ID Data Begin ---------
|
||||
|
||||
def _make_host_api_get_request(host_ip, token, endpoint):
|
||||
url = f"https://{host_ip}/{endpoint}"
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
response = requests.get(url, headers=headers, verify=False)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
def get_system_info(host_ip, token):
|
||||
return _make_host_api_get_request(host_ip, token, "core/pls/api/1/system/info")
|
||||
|
||||
def get_site_info(host_ip, token):
|
||||
return _make_host_api_get_request(host_ip, token, "core/pls/api/1/site/info")
|
||||
|
||||
def get_frontend_config(host_ip, token):
|
||||
return _make_host_api_get_request(host_ip, token, "frontend/config")
|
||||
|
||||
def get_licensed_host_info(host_ip, token):
|
||||
"""Retrieves host info from the /mgt/host endpoint."""
|
||||
url = f"https://{host_ip}/core/licensed/api/1/mgt/host"
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
response = requests.get(url, headers=headers, verify=False)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
def get_licenses_info(host_ip, token):
|
||||
url = f"https://{host_ip}/core/licensed/api/1/mgt/licenses"
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
response = requests.get(url, headers=headers, verify=False)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
def get_host_details(host_ip):
|
||||
token = auth_utils.authenticate(host_ip)
|
||||
|
||||
system_info = get_system_info(host_ip, token)
|
||||
site_info = get_site_info(host_ip, token)
|
||||
frontend_config = get_frontend_config(host_ip, token)
|
||||
licensed_host_info = get_licensed_host_info(host_ip, token)
|
||||
licenses_info = get_licenses_info(host_ip, token)
|
||||
|
||||
license_map = {}
|
||||
if isinstance(licenses_info, list):
|
||||
for lic in licenses_info:
|
||||
app_content = lic.get('license', {}).get('app_content', {})
|
||||
app_type = app_content.get('app_type')
|
||||
if app_type:
|
||||
license_map[app_type] = lic
|
||||
|
||||
services_with_licenses = []
|
||||
for service in frontend_config.get("services", []):
|
||||
matching_license = license_map.get(service.get('name'))
|
||||
if matching_license:
|
||||
params = matching_license.get('license', {}).get('license_params', {})
|
||||
start_epoch = params.get('start_date')
|
||||
expire_epoch = params.get('expire_date')
|
||||
|
||||
if isinstance(start_epoch, (int, float)):
|
||||
params['start_date_str'] = datetime.fromtimestamp(start_epoch).strftime('%Y-%m-%d')
|
||||
if isinstance(expire_epoch, (int, float)):
|
||||
params['expire_date_str'] = datetime.fromtimestamp(expire_epoch).strftime('%Y-%m-%d')
|
||||
|
||||
service['license'] = matching_license
|
||||
services_with_licenses.append(service)
|
||||
|
||||
main_license = licenses_info[0] if licenses_info else None
|
||||
if main_license:
|
||||
params = main_license.get('license', {}).get('license_params', {})
|
||||
start_epoch = params.get('start_date')
|
||||
expire_epoch = params.get('expire_date')
|
||||
if isinstance(start_epoch, (int, float)):
|
||||
params['start_date_str'] = datetime.fromtimestamp(start_epoch).strftime('%Y-%m-%d')
|
||||
if isinstance(expire_epoch, (int, float)):
|
||||
params['expire_date_str'] = datetime.fromtimestamp(expire_epoch).strftime('%Y-%m-%d')
|
||||
|
||||
combined_data = {
|
||||
"system": system_info,
|
||||
"site": site_info,
|
||||
"services": services_with_licenses,
|
||||
"licensed_host": licensed_host_info,
|
||||
"license": main_license
|
||||
}
|
||||
return combined_data
|
||||
|
||||
# ------- System ID Data End ---------
|
||||
|
||||
|
||||
def create_backup(host_ip, token):
|
||||
url = f"https://{host_ip}/core/pls/api/1/backup/create"
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
payload = {
|
||||
"services": [
|
||||
"amf", "ausf", "bmsc", "chf", "dra", "dsm", "eir", "mme", "smsf",
|
||||
"licensed", "nrf", "pcf", "aaa", "sgwc", "smf", "udm", "udr",
|
||||
"upf", "ncm", "pls"
|
||||
]
|
||||
}
|
||||
response = requests.post(url, headers=headers, json=payload, verify=False, stream=True)
|
||||
response.raise_for_status()
|
||||
return response
|
||||
|
||||
def list_users(base_url, token, session):
|
||||
url = f"{base_url}/portal/api/tenants/users"
|
||||
headers = session.headers.copy()
|
||||
headers["horus-token"] = token
|
||||
response = session.get(url, headers=headers, verify=False)
|
||||
response.raise_for_status()
|
||||
return response.json().get("users", [])
|
||||
|
||||
def generate_m2000_password(serial, seed="ANWEP5G", prefix="EP5G"):
|
||||
seed_serial = seed + serial
|
||||
sha_dig = hashlib.sha256(seed_serial.encode('utf-8')).hexdigest()
|
||||
pointer = int(sha_dig[0], 16)
|
||||
twelve = sha_dig[pointer:pointer+12]
|
||||
password = f"{prefix}!{twelve}"
|
||||
return password
|
||||
|
||||
def reset_m2000_configuration(base_ipv6):
|
||||
"""
|
||||
Resets specified services on two hosts derived from a base IPv6 address.
|
||||
"""
|
||||
# List of services to be reset
|
||||
services_to_reset = ["amf", "upf", "smf", "sgwc", "mme", "pcf"]
|
||||
|
||||
# Derive the two host IPs
|
||||
# This assumes the base address ends with something like ':0' or '::'
|
||||
base_parts = base_ipv6.rsplit(':', 1)
|
||||
if len(base_parts) < 2:
|
||||
raise ValueError("Invalid IPv6 address format for deriving hosts.")
|
||||
|
||||
base_prefix = base_parts[0]
|
||||
host_a_ip = f"{base_prefix}:a"
|
||||
host_b_ip = f"{base_prefix}:b"
|
||||
hosts = [host_a_ip, host_b_ip]
|
||||
|
||||
results = []
|
||||
|
||||
for host in hosts:
|
||||
try:
|
||||
# Authenticate with the current host
|
||||
token = auth_utils.authenticate(host)
|
||||
|
||||
for service in services_to_reset:
|
||||
try:
|
||||
# Construct the specific reset URL for each service
|
||||
url = f"https://[{host}]/core/{service}/api/1/mgmt/config/factory_reset"
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
# Make the POST request to trigger the reset
|
||||
response = requests.post(url, headers=headers, json={}, verify=False)
|
||||
response.raise_for_status()
|
||||
|
||||
results.append({"host": host, "service": service, "status": "Success", "message": "Reset command sent successfully."})
|
||||
|
||||
except HTTPError as http_err:
|
||||
results.append({"host": host, "service": service, "status": "Failed", "message": str(http_err)})
|
||||
|
||||
except Exception as e:
|
||||
results.append({"host": host, "service": "N/A", "status": "Connection Failed", "message": str(e)})
|
||||
|
||||
return results
|
||||
356
customers.txt
Normal file
356
customers.txt
Normal file
@@ -0,0 +1,356 @@
|
||||
001,Athonet
|
||||
002,Extenet
|
||||
003,COTA
|
||||
004,Protezione Civile FVG
|
||||
005,Enel - Wind (NSN)
|
||||
006,RadioAccess
|
||||
007,Samsung
|
||||
008,ComTel
|
||||
009,Smart Mobile Labs
|
||||
010,QuCell
|
||||
011,Nagra - Kudelski
|
||||
012,AirBus Defense and Space (Cassidian)
|
||||
013,AirBus Defense and Space (Astrium)
|
||||
014,Vitrociset
|
||||
015,Fraunhofer
|
||||
016,Smartgrid - Brindisi
|
||||
017,Nokia
|
||||
018,Vodafone Group
|
||||
019,Access Communications
|
||||
020,RedLine
|
||||
021,NEC
|
||||
022,Motorola
|
||||
023,Alfa-Consult
|
||||
024,Alcatel Lucent
|
||||
025,JRC
|
||||
026,British Telecom
|
||||
027,Smartgrid - Enel MVNO
|
||||
028,HMS
|
||||
029,Nokia Swiss RUAG
|
||||
030,Afrimax
|
||||
031,Inno Wireless
|
||||
032,Huawei
|
||||
033,Nash
|
||||
034,Smartfren
|
||||
035,Multisource
|
||||
036,Ambra
|
||||
037,Facebook
|
||||
038,Telrad
|
||||
039,Sprint
|
||||
040,118 Lombardia
|
||||
041,Baicells
|
||||
042,Insis
|
||||
043,Telecom Italia
|
||||
044,Oracle
|
||||
045,ABIOM
|
||||
046,Yes 4G Mozambico
|
||||
047,FCA - Torino
|
||||
048,Dolphin Telecom
|
||||
049,Mavenir
|
||||
050,Telus
|
||||
051,Comcast
|
||||
052,OTE
|
||||
053,Transit Wireless
|
||||
054,mob5g - Broadband Systems
|
||||
055,Lundin Petroleum
|
||||
056,Tiscali
|
||||
057,Armasuisse
|
||||
058,IIT Demokritos
|
||||
059,Servizi Segreti
|
||||
060,OptimERA
|
||||
061,Future Technologies Venture, LLC
|
||||
062,SARI - TIM
|
||||
063,Premier Broadband
|
||||
064,Boingo Wireless
|
||||
065,Ruckus
|
||||
066,TDF
|
||||
067,Airspan Networks
|
||||
068,Telia
|
||||
069,A2A smart city
|
||||
070,University of Malaga - LCC
|
||||
071,Klas Telecom
|
||||
072,PrimeTel
|
||||
073,Marubun
|
||||
074,M3CONNECT
|
||||
075,Digital Catapult
|
||||
076,Tropiconet
|
||||
077,Becker
|
||||
078,SPM Telecom
|
||||
079,SoftBank - Marubun
|
||||
080,CRS4
|
||||
081,ESA - Agenzia Spaziale Europea
|
||||
082,ELTA
|
||||
083,Gendarmerie Nationele
|
||||
084,Iskratel
|
||||
085,Unitel - Capo Verde
|
||||
086,Telstra
|
||||
087,Digital Communications Consulting (DCC)
|
||||
088,Axione
|
||||
089,Connectivity Wireless, LLC
|
||||
090,WIND-TRE
|
||||
091,FastWeb
|
||||
092,Surf Telecom
|
||||
093,NFL - National Footbal League
|
||||
094,EDZCOM
|
||||
095,Portugal Telecom
|
||||
096,Polizia Spagnola
|
||||
097,HUB ONE
|
||||
098,Esseti Sistemi e Tecnologie Srl
|
||||
099,VMWare US
|
||||
100,FreshWave
|
||||
101,BMW AG
|
||||
102,Cisco Systems
|
||||
103,Honeywell Technology solutions
|
||||
104,NEOM Project
|
||||
105,VMware UK Limited
|
||||
106,Palo Alto Networks
|
||||
107,JMA - TIM
|
||||
108,EDF
|
||||
109,NextLink, CBRS Fixed Wireless
|
||||
110,Mediacom
|
||||
111,TowerCast
|
||||
112,CapX
|
||||
113,COMGU
|
||||
114,ZAYO
|
||||
115,Kaina-Com
|
||||
116,Telecom Italia (MILITARY)
|
||||
117,TechMahindra
|
||||
118,SkyFive AG
|
||||
119,Leonardo
|
||||
120,InterMax Networks
|
||||
121,KLA Lab - Ford Motor
|
||||
122,Vilicom
|
||||
123,Stratto Ltd - Freshwave
|
||||
124,T3
|
||||
125,Murray School
|
||||
126,ClearSky
|
||||
127,Enel Chile
|
||||
128,Innovate 5G
|
||||
129,AWS
|
||||
130,Electronic Media Services Limited
|
||||
131,Tilson Technology Management, Inc.
|
||||
132,World Wide Technology, Inc. - WWT
|
||||
133,Switch Inc.
|
||||
134,Caterpillar Inc.
|
||||
135,SRCWireless, LLC
|
||||
136,Jimsontec corporation
|
||||
137,IBM Global Business Services
|
||||
138,COMMSCOPE
|
||||
139,Dinuba Unified School District
|
||||
140,McFarland Unified School District
|
||||
141,Telenor
|
||||
142,Semco Maritime
|
||||
143,Dallas Indipendent School District
|
||||
144,Enel Spain
|
||||
145,BearCom
|
||||
146,CDW
|
||||
147,Merced College
|
||||
148,Enel Brazil
|
||||
149,Intel Corporation
|
||||
150,Miller Electric Co.
|
||||
151,Securus
|
||||
152,IQThings
|
||||
153,FBK - Fondazione Bruno Kessler
|
||||
154,Colt
|
||||
155,Evolve Cellular
|
||||
156,Telnyx
|
||||
157,Fortress Solutions
|
||||
158,Consort Digital
|
||||
159,Dubai Petroleum
|
||||
160,Cellnex Telecom S.A.
|
||||
161,Corbec
|
||||
162,STL - Sterlite Technologies Ltd.
|
||||
163,Aramco
|
||||
164,TIM Outpost AWS
|
||||
165,ATS Elektronik GmbH
|
||||
166,ICT Consulting Andmore s.r.o.
|
||||
167,Accenture
|
||||
168,Nexstream
|
||||
169,Italtel
|
||||
170,Wireless Infrastructure Group
|
||||
171,Exor International S.p.A.
|
||||
172,Pivotel
|
||||
173,Iconec GmbH
|
||||
174,ChannelTeq
|
||||
175,GRID
|
||||
176,VTS
|
||||
177,Rakuten
|
||||
178,Parallel Wireless
|
||||
179,ADM
|
||||
180,Tata Communications Ltd
|
||||
181,A1 Austria
|
||||
182,Vislink
|
||||
183,Railspire
|
||||
184,Radlink Communications
|
||||
185,Etisalat
|
||||
186,Telent
|
||||
187,Zeetta
|
||||
188,Omantel
|
||||
189,Cellcom
|
||||
190,Kontron
|
||||
191,Diverse Power
|
||||
192,Federated Wireless
|
||||
193,HTC Corporation
|
||||
194,Orbitica
|
||||
195,MECSware
|
||||
196,Alea
|
||||
197,LGE
|
||||
198,Kapsch
|
||||
199,The Scotland 5G Centre
|
||||
200,Arqueirotelecom
|
||||
201,Systemics PAB
|
||||
202,HHI
|
||||
203,Anterix
|
||||
204,Amdocs
|
||||
205,Acromove
|
||||
206,Atmosphere
|
||||
207,IES-Italia
|
||||
208,BoostPro Systems
|
||||
209,Telespazio
|
||||
210,Larsen&Toubro
|
||||
211,BesCom
|
||||
212,Qualcomm
|
||||
213,ViaSat
|
||||
214,GibFibre
|
||||
215,ThinkSmartWay
|
||||
216,Drei H3G Austria
|
||||
217,MASSILATECH
|
||||
218,Wildanet
|
||||
219,HFCL
|
||||
220,SCHEIDER ELECTRIC
|
||||
221,Juniper
|
||||
222,atlantis-group
|
||||
223,Flash Private Mobile Networks
|
||||
224,Nextivity
|
||||
225,Selectric
|
||||
226,LUFTHANSA
|
||||
227,Falcon Internet
|
||||
228,Texas A&M University
|
||||
229,University of Hannover
|
||||
230,OIV
|
||||
231,Mountain View Whisman School District
|
||||
232,T3 Wireless
|
||||
233,Netcon Technologies
|
||||
234,The Bakery Tech Limited
|
||||
235,SYSOCO
|
||||
236,DFKI
|
||||
237,Spi
|
||||
238,Politecnico di Milano
|
||||
239,MF Wireless
|
||||
240,Metracom
|
||||
241,Cedarview Technology Private Limited
|
||||
242,Alcon
|
||||
243,Infosys
|
||||
244,Digi
|
||||
245,Capgemini
|
||||
246,Deloitte Central Services
|
||||
247,Transocean
|
||||
248,RAA Data Services
|
||||
249,University of Virgina
|
||||
250,LKAB
|
||||
251,Vantage Towers
|
||||
252,Sunwave
|
||||
253,Acceleran
|
||||
254,Webbings
|
||||
255,Fujitsu
|
||||
256,Telefonica
|
||||
257,Opnet
|
||||
258,Aura Networks
|
||||
259,Arqit Limited
|
||||
260,RSE
|
||||
261,Asocs
|
||||
262,NetSF
|
||||
263,Mugler
|
||||
264,Airtel
|
||||
265,American Tower Company
|
||||
266,HMF Smart Solutions
|
||||
267,NTT data
|
||||
268,NAVSEA
|
||||
269,University of Torino
|
||||
270,ISP Supplies
|
||||
271,Citymesh
|
||||
272,NTT Data Italia
|
||||
273,Aeronautica Militare
|
||||
274,Shared Access Ltd
|
||||
275,iNET - Infrastructure Networks
|
||||
276,HPE
|
||||
277,Deloitte Consulting GmbH
|
||||
278,Sepura
|
||||
279,CGI IT UK Limited
|
||||
280,AirBus (SWB)
|
||||
281,DY Logistics
|
||||
282,Pierson Wireless
|
||||
283,XFone
|
||||
284,Indosat
|
||||
285,Maruthi
|
||||
286,Nebulae
|
||||
287,KPN
|
||||
288,Esercito Italiano
|
||||
289,Altel
|
||||
290,Boldyn
|
||||
291,Bakertilly
|
||||
292,Hotwire
|
||||
293,UltraNetworks
|
||||
294,Mobicom
|
||||
295,GE Health Care
|
||||
296,Ligado
|
||||
297,Tampnet
|
||||
298,Wireless Partners
|
||||
299,Università Tor Vergata
|
||||
300,A1 Slovenia
|
||||
301,Terna (ITALTEL)
|
||||
302,America Movile
|
||||
303,Eldorado
|
||||
304,vertext
|
||||
305,UPV Universitat Politècnica de València
|
||||
306,5G-EMERGE
|
||||
307,STC POC
|
||||
308,TDC NET
|
||||
309,Deloitte India
|
||||
310,Askey
|
||||
311,MoD Thailand
|
||||
312,Telos
|
||||
313,NorthCom
|
||||
314,HOSTS-SAT
|
||||
315,Teledife
|
||||
316,SPIE
|
||||
317,UNIGENOVA
|
||||
318,Logicalis
|
||||
319,SPIE-Isala
|
||||
320,Neptune
|
||||
321,Really
|
||||
322,Groove City
|
||||
323,spark
|
||||
324,ConnectCom
|
||||
325,Lnett
|
||||
326,AWTG
|
||||
327,Belden
|
||||
328,Sita
|
||||
329,SNCF
|
||||
330,Sercomm
|
||||
331,MSBenbow
|
||||
332,Lockheed Martin
|
||||
333,WIG
|
||||
334,boldyn-uk
|
||||
335,SouthWest Research Institute SWRI
|
||||
336,Special Operations Command SOCOM
|
||||
337,GAI
|
||||
338,Norseman
|
||||
339,Restart
|
||||
340,AXIANS
|
||||
341,Aricoma
|
||||
342,Boldyn Networks
|
||||
343,Teleios
|
||||
344,ENVELOPE
|
||||
345,ADS
|
||||
346,CSEC
|
||||
347,Delta Solutions
|
||||
348,National Grid UK
|
||||
349,Tamu
|
||||
350,IIJ
|
||||
351,RIOTINTI
|
||||
352,ONE
|
||||
353,NOVA Greece
|
||||
354,DXC
|
||||
355,V-Valley
|
||||
356,CityCom
|
||||
1
frontend_config.json
Normal file
1
frontend_config.json
Normal file
@@ -0,0 +1 @@
|
||||
{"services":[{"enabled":true,"name":"alertmanager","type":"ps","version":"0.28.1","state":"started","mgmt":false,"readonly":false,"backup":false,"autostart":true},{"enabled":true,"name":"amf","type":"nf","version":"2.3.2","state":"started","mgmt":true,"readonly":false,"backup":true,"autostart":true},{"enabled":true,"name":"ausf","type":"nf","version":"1.12.1","state":"started","mgmt":true,"readonly":false,"backup":true,"autostart":true},{"enabled":true,"name":"bmsc","type":"nf","version":"0.6.3","state":"started","mgmt":true,"readonly":false,"backup":true,"autostart":true},{"enabled":true,"name":"chf","type":"nf","version":"4.17.3","state":"started","mgmt":true,"readonly":false,"backup":true,"autostart":true},{"enabled":true,"name":"dra","type":"nf","version":"1.9.1","state":"started","mgmt":true,"readonly":false,"backup":true,"autostart":true},{"enabled":true,"name":"dsm","type":"ps","version":"0.12.1","state":"started","mgmt":true,"readonly":false,"backup":true,"autostart":true},{"enabled":true,"name":"eir","type":"nf","version":"1.12.1","state":"started","mgmt":true,"readonly":false,"backup":true,"autostart":true},{"enabled":true,"name":"mme","type":"nf","version":"2.6.4","state":"started","mgmt":true,"readonly":false,"backup":true,"autostart":true},{"enabled":true,"name":"smsf","type":"nf","version":"0.19.4","state":"started","mgmt":true,"readonly":false,"backup":true,"autostart":true},{"enabled":true,"name":"fluent-bit","type":"ps","version":"3.2.10","state":"started","mgmt":false,"readonly":false,"backup":false,"autostart":true},{"enabled":false,"name":"gatewayd","type":"ps","version":"-","state":"started","mgmt":false,"readonly":true,"backup":false,"autostart":false},{"enabled":true,"name":"grafana","type":"ps","version":"11.5.2","state":"started","mgmt":false,"readonly":false,"backup":false,"autostart":true},{"enabled":true,"name":"licensed","type":"ps","version":"2.6.3","state":"started","mgmt":true,"readonly":false,"backup":true,"autostart":true},{"enabled":true,"name":"node-exporter","type":"ps","version":"1.9.0","state":"started","mgmt":false,"readonly":false,"backup":false,"autostart":true},{"enabled":true,"name":"nrf","type":"nf","version":"1.14.1","state":"started","mgmt":true,"readonly":false,"backup":true,"autostart":true},{"enabled":true,"name":"pcf","type":"nf","version":"1.4.1","state":"started","mgmt":true,"readonly":false,"backup":true,"autostart":true},{"enabled":true,"name":"aaa","type":"nf","version":"1.1.7","state":"started","mgmt":true,"readonly":false,"backup":true,"autostart":true},{"enabled":true,"name":"podman-exporter","type":"ps","version":"1.15.0","state":"started","mgmt":false,"readonly":false,"backup":false,"autostart":true},{"enabled":true,"name":"prometheus","type":"ps","version":"3.2.1","state":"started","mgmt":false,"readonly":false,"backup":false,"autostart":true},{"enabled":true,"name":"sgwc","type":"nf","version":"0.12.2","state":"started","mgmt":true,"readonly":false,"backup":true,"autostart":true},{"enabled":true,"name":"smf","type":"nf","version":"1.20.5","state":"started","mgmt":true,"readonly":false,"backup":true,"autostart":true},{"enabled":false,"name":"swupdate","type":"ps","version":"-","state":"started","mgmt":false,"readonly":true,"backup":false,"autostart":false},{"enabled":true,"name":"webconsole","type":"ps","version":"1.1.4","state":"started","mgmt":false,"readonly":false,"backup":false,"autostart":true},{"enabled":true,"name":"udm","type":"nf","version":"1.10.1","state":"started","mgmt":true,"readonly":false,"backup":true,"autostart":true},{"enabled":true,"name":"udr","type":"nf","version":"1.10.1","state":"started","mgmt":true,"readonly":false,"backup":true,"autostart":true},{"enabled":true,"name":"upf","type":"nf","version":"1.17.9","state":"started","mgmt":true,"readonly":false,"backup":true,"autostart":true},{"enabled":true,"name":"ncm","type":"ps","version":"0.22.2","state":"started","mgmt":false,"readonly":true,"backup":true,"autostart":true},{"enabled":false,"name":"pls","type":"ps","version":"1.2.2","state":"started","mgmt":true,"readonly":true,"backup":true,"autostart":false},{"enabled":true,"name":"openvpn","type":"ps","version":"-","state":"started","mgmt":false,"readonly":false,"backup":false,"autostart":true},{"enabled":true,"name":"ssh","type":"ps","version":"-","state":"started","mgmt":false,"readonly":false,"backup":false,"autostart":true},{"enabled":false,"name":"keepalived-exporter","type":"ps","version":"-","state":"stopped","mgmt":false,"readonly":false,"backup":false,"autostart":false}],"ui_inactivity_s":null,"auth_backends":["local"],"system_terms":{"enabled":false,"terms":"","forbidden_notice":""}}
|
||||
24
generate_ep5g_password.py
Normal file
24
generate_ep5g_password.py
Normal file
@@ -0,0 +1,24 @@
|
||||
import hashlib
|
||||
import argparse
|
||||
|
||||
# Set up command line argument parsing
|
||||
parser = argparse.ArgumentParser(description="Generate EP5G password from serial.")
|
||||
parser.add_argument("serial", help="Hardware serial number")
|
||||
parser.add_argument("-s", "--seed", default="ANWEP5G", help="Seed value (default: ANWEP5G)")
|
||||
parser.add_argument("-p", "--prefix", default="EP5G", help="Password prefix (default: EP5G)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Combine seed and serial
|
||||
seed_serial = args.seed + args.serial
|
||||
|
||||
# SHA-256 hash
|
||||
sha_dig = hashlib.sha256(seed_serial.encode('utf-8')).hexdigest()
|
||||
|
||||
# Extract password portion
|
||||
pointer = int(sha_dig[0], 16)
|
||||
twelve = sha_dig[pointer:pointer+12]
|
||||
password = f"{args.prefix}!{twelve}"
|
||||
|
||||
# Output
|
||||
print(password)
|
||||
5
requirements.txt
Normal file
5
requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
# requirements.txt
|
||||
Flask
|
||||
requests
|
||||
gunicorn
|
||||
paramiko
|
||||
BIN
services/__pycache__/combocore.cpython-310.pyc
Normal file
BIN
services/__pycache__/combocore.cpython-310.pyc
Normal file
Binary file not shown.
BIN
services/__pycache__/local_net.cpython-310.pyc
Normal file
BIN
services/__pycache__/local_net.cpython-310.pyc
Normal file
Binary file not shown.
BIN
services/__pycache__/net_info.cpython-310.pyc
Normal file
BIN
services/__pycache__/net_info.cpython-310.pyc
Normal file
Binary file not shown.
BIN
services/__pycache__/remote_admin.cpython-310.pyc
Normal file
BIN
services/__pycache__/remote_admin.cpython-310.pyc
Normal file
Binary file not shown.
BIN
services/__pycache__/state.cpython-310.pyc
Normal file
BIN
services/__pycache__/state.cpython-310.pyc
Normal file
Binary file not shown.
BIN
services/__pycache__/yaml_writer.cpython-310.pyc
Normal file
BIN
services/__pycache__/yaml_writer.cpython-310.pyc
Normal file
Binary file not shown.
2
services/api/cc_v1/init.py
Normal file
2
services/api/cc_v1/init.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from flask import Blueprint
|
||||
bp = Blueprint("cc_v1", __name__)
|
||||
25
services/api/cc_v1/ncm.py
Normal file
25
services/api/cc_v1/ncm.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from flask import Blueprint, request, jsonify, current_app
|
||||
from services.combocore.ncm import get_routes, derive_eth0_cidr_gw
|
||||
|
||||
bp = Blueprint("cc_ncm_v1", __name__)
|
||||
|
||||
@bp.post("/routes")
|
||||
def api_routes():
|
||||
body = request.get_json(silent=True) or {}
|
||||
host = body.get("host", "").strip()
|
||||
if not host:
|
||||
return jsonify({"ok": False, "error": "host required"}), 400
|
||||
cfg = current_app.config
|
||||
routes = get_routes(host, cfg["CORE_API_USER"], cfg["CORE_API_PASS"], cfg["VERIFY_SSL"], cfg["REQUEST_TIMEOUT"])
|
||||
return jsonify({"ok": True, "routes": routes})
|
||||
|
||||
@bp.post("/oam")
|
||||
def api_oam_eth0():
|
||||
body = request.get_json(silent=True) or {}
|
||||
host = body.get("host", "").strip()
|
||||
if not host:
|
||||
return jsonify({"ok": False, "error": "host required"}), 400
|
||||
cfg = current_app.config
|
||||
routes = get_routes(host, cfg["CORE_API_USER"], cfg["CORE_API_PASS"], cfg["VERIFY_SSL"], cfg["REQUEST_TIMEOUT"])
|
||||
cidr, gw = derive_eth0_cidr_gw(routes)
|
||||
return jsonify({"ok": True, "cidr": cidr, "gw": gw})
|
||||
24
services/api/cc_v1/pls.py
Normal file
24
services/api/cc_v1/pls.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from flask import Blueprint, request, jsonify, current_app
|
||||
from services.combocore.pls import login as pls_login, get_me
|
||||
|
||||
bp = Blueprint("cc_pls_v1", __name__)
|
||||
|
||||
@bp.post("/login")
|
||||
def api_login():
|
||||
body = request.get_json(silent=True) or {}
|
||||
host = body.get("host", "").strip()
|
||||
if not host:
|
||||
return jsonify({"ok": False, "error": "host required"}), 400
|
||||
cfg = current_app.config
|
||||
token = pls_login(host, cfg["CORE_API_USER"], cfg["CORE_API_PASS"], cfg["VERIFY_SSL"], cfg["REQUEST_TIMEOUT"])
|
||||
return jsonify({"ok": True, "access_token": token})
|
||||
|
||||
@bp.post("/me")
|
||||
def api_me():
|
||||
body = request.get_json(silent=True) or {}
|
||||
host = body.get("host", "").strip()
|
||||
if not host:
|
||||
return jsonify({"ok": False, "error": "host required"}), 400
|
||||
cfg = current_app.config
|
||||
me = get_me(host, cfg["CORE_API_USER"], cfg["CORE_API_PASS"], cfg["VERIFY_SSL"], cfg["REQUEST_TIMEOUT"])
|
||||
return jsonify({"ok": True, "me": me})
|
||||
63
services/combocore.py
Normal file
63
services/combocore.py
Normal file
@@ -0,0 +1,63 @@
|
||||
# services/combocore.py
|
||||
import requests
|
||||
from requests.exceptions import HTTPError
|
||||
|
||||
requests.packages.urllib3.disable_warnings()
|
||||
|
||||
def _fmt_host(host: str) -> str:
|
||||
host = host.strip().strip("[]")
|
||||
# if IPv6, wrap with []
|
||||
if ":" in host and not host.startswith("["):
|
||||
return f"[{host}]"
|
||||
return host
|
||||
|
||||
def login(host: str, username="admin", password="Super4dmin!") -> str:
|
||||
h = _fmt_host(host)
|
||||
url = f"https://{h}/core/pls/api/1/auth/login"
|
||||
r = requests.post(url, json={"username": username, "password": password}, verify=False, timeout=10)
|
||||
r.raise_for_status()
|
||||
j = r.json()
|
||||
# API returns "access_token"
|
||||
return j["access_token"]
|
||||
|
||||
def get_routes(host: str, token: str) -> list:
|
||||
h = _fmt_host(host)
|
||||
url = f"https://{h}/core/ncm/api/1/status/routes"
|
||||
r = requests.get(url, headers={"Authorization": f"Bearer {token}"}, verify=False, timeout=10)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
def extract_eth0_cidr_gw(routes: list) -> tuple[str, str]:
|
||||
"""
|
||||
From the routes list:
|
||||
- CIDR = <prefsrc>/<mask-from-eth0-connected-route>
|
||||
- GW = default route gateway on eth0
|
||||
"""
|
||||
prefsrc = None
|
||||
mask = None
|
||||
gw = None
|
||||
|
||||
# default gw via eth0
|
||||
for r in routes:
|
||||
if r.get("family") == "inet" and r.get("type") == "unicast" and r.get("dst") == "default" and r.get("dev") == "eth0":
|
||||
gw = r.get("gateway")
|
||||
break
|
||||
|
||||
# find eth0 connected network to derive mask, and prefsrc for IP
|
||||
for r in routes:
|
||||
if r.get("family") == "inet" and r.get("dev") == "eth0":
|
||||
if not prefsrc and r.get("prefsrc"):
|
||||
prefsrc = r.get("prefsrc")
|
||||
dst = r.get("dst") or ""
|
||||
if "/" in dst:
|
||||
try:
|
||||
mask = dst.split("/", 1)[1]
|
||||
except Exception:
|
||||
pass
|
||||
if prefsrc and mask and gw:
|
||||
break
|
||||
|
||||
if not (prefsrc and mask and gw):
|
||||
raise ValueError("Unable to derive eth0 CIDR/GW from routes payload")
|
||||
|
||||
return f"{prefsrc}/{mask}", gw
|
||||
54
services/combocore/client.py
Normal file
54
services/combocore/client.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from __future__ import annotations
|
||||
import requests
|
||||
from requests.adapters import HTTPAdapter
|
||||
from urllib3.util.retry import Retry
|
||||
|
||||
DEFAULT_TIMEOUT = 10.0
|
||||
|
||||
class ComboCoreClient:
|
||||
def __init__(self, host: str, username: str, password: str, verify_ssl: bool = False, timeout: float = DEFAULT_TIMEOUT):
|
||||
self.base = f"https://{host}"
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.verify = verify_ssl
|
||||
self.timeout = timeout
|
||||
self._token = None
|
||||
self._s = requests.Session()
|
||||
retry = Retry(total=3, backoff_factor=0.3, status_forcelist=[429,500,502,503,504])
|
||||
self._s.mount("https://", HTTPAdapter(max_retries=retry))
|
||||
self._s.mount("http://", HTTPAdapter(max_retries=retry))
|
||||
|
||||
# ----- PLS auth -----
|
||||
def login(self) -> str:
|
||||
r = self._s.post(
|
||||
f"{self.base}/core/pls/api/1/auth/login",
|
||||
json={"username": self.username, "password": self.password},
|
||||
timeout=self.timeout, verify=self.verify
|
||||
)
|
||||
r.raise_for_status()
|
||||
j = r.json()
|
||||
self._token = j.get("access_token") or j.get("token")
|
||||
if not self._token:
|
||||
raise RuntimeError("No access token in login response.")
|
||||
return self._token
|
||||
|
||||
def _auth_headers(self):
|
||||
if not self._token:
|
||||
self.login()
|
||||
return {"Authorization": f"Bearer {self._token}"}
|
||||
|
||||
def get(self, path: str):
|
||||
r = self._s.get(f"{self.base}{path}", headers=self._auth_headers(), timeout=self.timeout, verify=self.verify)
|
||||
if r.status_code == 401:
|
||||
self._token = None
|
||||
r = self._s.get(f"{self.base}{path}", headers=self._auth_headers(), timeout=self.timeout, verify=self.verify)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
def post(self, path: str, json=None):
|
||||
r = self._s.post(f"{self.base}{path}", headers=self._auth_headers(), json=json or {}, timeout=self.timeout, verify=self.verify)
|
||||
if r.status_code == 401:
|
||||
self._token = None
|
||||
r = self._s.post(f"{self.base}{path}", headers=self._auth_headers(), json=json or {}, timeout=self.timeout, verify=self.verify)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
29
services/combocore/ncm.py
Normal file
29
services/combocore/ncm.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from typing import Tuple, Optional, List, Dict
|
||||
from .client import ComboCoreClient
|
||||
|
||||
def get_routes(host: str, user: str, pwd: str, verify_ssl=False, timeout=10.0) -> List[Dict]:
|
||||
cli = ComboCoreClient(host, user, pwd, verify_ssl, timeout)
|
||||
return cli.get("/core/ncm/api/1/status/routes")
|
||||
|
||||
def derive_eth0_cidr_gw(routes: List[Dict]) -> Tuple[str, Optional[str]]:
|
||||
gw = None
|
||||
ip = None
|
||||
masklen = None
|
||||
for rt in routes:
|
||||
if rt.get("family") == "inet" and rt.get("dst") == "default" and rt.get("dev") == "eth0":
|
||||
gw = rt.get("gateway")
|
||||
for rt in routes:
|
||||
if rt.get("family") == "inet" and rt.get("dev") == "eth0":
|
||||
if not ip and rt.get("prefsrc"):
|
||||
ip = rt["prefsrc"]
|
||||
dst = rt.get("dst", "")
|
||||
if "/" in dst:
|
||||
try:
|
||||
masklen = int(dst.split("/", 1)[1])
|
||||
except Exception:
|
||||
pass
|
||||
if ip and masklen is not None:
|
||||
break
|
||||
if not ip or masklen is None:
|
||||
raise RuntimeError("Could not derive eth0 IP/prefix from routes.")
|
||||
return f"{ip}/{masklen}", gw
|
||||
9
services/combocore/pls.py
Normal file
9
services/combocore/pls.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from .client import ComboCoreClient
|
||||
|
||||
# Minimal PLS surface to start
|
||||
def login(host: str, user: str, pwd: str, verify_ssl=False, timeout=10.0) -> str:
|
||||
return ComboCoreClient(host, user, pwd, verify_ssl, timeout).login()
|
||||
|
||||
def get_me(host: str, user: str, pwd: str, verify_ssl=False, timeout=10.0) -> dict:
|
||||
cli = ComboCoreClient(host, user, pwd, verify_ssl, timeout)
|
||||
return cli.get("/core/pls/api/1/auth/me") # adjust if different
|
||||
42
services/local_net.py
Normal file
42
services/local_net.py
Normal file
@@ -0,0 +1,42 @@
|
||||
# services/local_net.py
|
||||
import subprocess
|
||||
import re
|
||||
|
||||
def _sh(cmd):
|
||||
out = subprocess.check_output(cmd, text=True)
|
||||
return out
|
||||
|
||||
def get_eth0_dhcp_snapshot():
|
||||
"""
|
||||
Returns {'iface':'eth0','cidr':'X.X.X.X/YY','ip':'X.X.X.X','prefixlen':YY,'gw':'A.B.C.D'}
|
||||
Raises on failure.
|
||||
"""
|
||||
iface = "eth0"
|
||||
|
||||
# ip -o -4 addr show dev eth0 | awk '{print $4}'
|
||||
addrs = _sh(["/sbin/ip", "-o", "-4", "addr", "show", "dev", iface]).strip()
|
||||
if not addrs:
|
||||
addrs = _sh(["ip", "-o", "-4", "addr", "show", "dev", iface]).strip()
|
||||
|
||||
m = re.search(r"\s(\d+\.\d+\.\d+\.\d+/\d+)\s", addrs)
|
||||
if not m:
|
||||
raise RuntimeError(f"No IPv4 address on {iface}")
|
||||
cidr = m.group(1)
|
||||
ip = cidr.split("/")[0]
|
||||
prefixlen = int(cidr.split("/")[1])
|
||||
|
||||
# ip route show default dev eth0 | awk '/default/ {print $3}'
|
||||
routes = _sh(["/sbin/ip", "route", "show", "default", "dev", iface])
|
||||
if not routes:
|
||||
routes = _sh(["ip", "route", "show", "default", "dev", iface])
|
||||
|
||||
m2 = re.search(r"default via (\d+\.\d+\.\d+\.\d+)", routes)
|
||||
gw = m2.group(1) if m2 else ""
|
||||
|
||||
return {
|
||||
"iface": iface,
|
||||
"cidr": cidr,
|
||||
"ip": ip,
|
||||
"prefixlen": prefixlen,
|
||||
"gw": gw
|
||||
}
|
||||
43
services/net_info.py
Normal file
43
services/net_info.py
Normal file
@@ -0,0 +1,43 @@
|
||||
# services/net_info.py
|
||||
import subprocess
|
||||
import json
|
||||
|
||||
def _run(cmd: list[str]) -> str:
|
||||
return subprocess.check_output(cmd, text=True).strip()
|
||||
|
||||
def _first_ipv4_cidr_from_ip_json(ip_json: dict) -> str | None:
|
||||
# Find first IPv4 on the interface
|
||||
for addr in ip_json.get("addr_info", []):
|
||||
if addr.get("family") == "inet" and addr.get("local") and addr.get("prefixlen") is not None:
|
||||
return f'{addr["local"]}/{addr["prefixlen"]}'
|
||||
return None
|
||||
|
||||
def get_iface_cidr(iface: str) -> str | None:
|
||||
# ip -j addr show dev eth0
|
||||
raw = _run(["ip", "-j", "addr", "show", "dev", iface])
|
||||
data = json.loads(raw)
|
||||
if not data:
|
||||
return None
|
||||
return _first_ipv4_cidr_from_ip_json(data[0])
|
||||
|
||||
def get_default_gw_for_iface(iface: str) -> str | None:
|
||||
lines = _run(["ip", "route", "show", "default", "dev", iface]).splitlines()
|
||||
for line in lines:
|
||||
parts = line.split()
|
||||
if parts and parts[0] == "default":
|
||||
try:
|
||||
idx = parts.index("via")
|
||||
return parts[idx + 1]
|
||||
except ValueError:
|
||||
continue
|
||||
except IndexError:
|
||||
continue
|
||||
return None
|
||||
|
||||
def get_eth0_dhcp_snapshot() -> dict:
|
||||
iface = "eth0"
|
||||
cidr = get_iface_cidr(iface) or ""
|
||||
gw = get_default_gw_for_iface(iface) or ""
|
||||
if not cidr or not gw:
|
||||
raise RuntimeError("Could not determine eth0 CIDR and/or default gateway")
|
||||
return {"iface": iface, "cidr": cidr, "gw": gw}
|
||||
48
services/remote_admin.py
Normal file
48
services/remote_admin.py
Normal file
@@ -0,0 +1,48 @@
|
||||
# services/remote_admin.py
|
||||
import requests, ipaddress
|
||||
|
||||
REQUEST_KW = dict(timeout=10, verify=False)
|
||||
|
||||
def validate_ipv4(ip: str) -> str:
|
||||
try:
|
||||
ipaddress.IPv4Address(ip)
|
||||
return ip
|
||||
except Exception:
|
||||
raise ValueError(f"Invalid IPv4 address: {ip}")
|
||||
|
||||
def authenticate(ip: str, username: str, password: str) -> str:
|
||||
url = f"https://{ip}/core/pls/api/1/auth/login"
|
||||
r = requests.post(url, json={"username": username, "password": password}, **REQUEST_KW)
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
token = data.get("access_token") or data.get("token") or data.get("data", {}).get("access_token")
|
||||
if not token:
|
||||
raise RuntimeError("No access token in auth response")
|
||||
return token
|
||||
|
||||
ALLOWED_SERVICES = {"ssh", "webconsole"}
|
||||
ALLOWED_ACTIONS = {"enable", "enable-autostart", "start"}
|
||||
|
||||
def service_action(ip: str, service: str, action: str, username: str, password: str) -> dict:
|
||||
if service not in ALLOWED_SERVICES:
|
||||
raise ValueError(f"Unsupported service: {service}")
|
||||
if action not in ALLOWED_ACTIONS:
|
||||
raise ValueError(f"Unsupported action: {action}")
|
||||
|
||||
ip = validate_ipv4(ip)
|
||||
token = authenticate(ip, username, password)
|
||||
url = f"https://{ip}/core/pls/api/1/services/{service}/{action}"
|
||||
r = requests.post(url, headers={"Authorization": f"Bearer {token}"}, **REQUEST_KW)
|
||||
r.raise_for_status()
|
||||
return r.json() if r.content else {"ok": True}
|
||||
|
||||
def perform_service_sequence(ip: str, service: str, username: str, password: str) -> None:
|
||||
for action in ("enable", "enable-autostart", "start"):
|
||||
service_action(ip, service, action, username, password)
|
||||
|
||||
# Backwards-compatible wrappers (optional)
|
||||
def ssh_action(ip, action, username, password):
|
||||
return service_action(ip, "ssh", action, username, password)
|
||||
|
||||
def perform_ssh_sequence(ip, username, password):
|
||||
return perform_service_sequence(ip, "ssh", username, password)
|
||||
31
services/state.py
Normal file
31
services/state.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# services/state.py
|
||||
from pathlib import Path
|
||||
import json
|
||||
|
||||
STATE_FILE = Path("system_info.json")
|
||||
|
||||
def load_state():
|
||||
if STATE_FILE.exists():
|
||||
return json.loads(STATE_FILE.read_text() or "{}")
|
||||
return {}
|
||||
|
||||
def save_state(d: dict):
|
||||
STATE_FILE.write_text(json.dumps(d, indent=2))
|
||||
|
||||
def set_target_ip(ip: str):
|
||||
st = load_state()
|
||||
st["target_host_ip"] = ip
|
||||
save_state(st)
|
||||
|
||||
def get_target_ip() -> str | None:
|
||||
return load_state().get("target_host_ip")
|
||||
|
||||
def set_mgmt_info(cidr: str, gw: str):
|
||||
"""Persist the current DHCP values we discovered for eth0 so we can render static OAM later."""
|
||||
st = load_state()
|
||||
st["mgmt"] = {"cidr": cidr, "gw": gw}
|
||||
save_state(st)
|
||||
|
||||
def get_mgmt_info() -> dict:
|
||||
"""Return {'cidr': 'x.x.x.x/yy', 'gw': 'x.x.x.x'} if previously captured, else {}."""
|
||||
return load_state().get("mgmt") or {}
|
||||
52
services/yaml_writer.py
Normal file
52
services/yaml_writer.py
Normal file
@@ -0,0 +1,52 @@
|
||||
# services/yaml_writer.py
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any
|
||||
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
||||
import shutil
|
||||
|
||||
ANSIBLE_ROOT = Path("ansible_workspace")
|
||||
STAGING = ANSIBLE_ROOT / "staging"
|
||||
TEMPLATE_DIR = Path("templates/ansible_templates")
|
||||
STATIC_SEEDS = TEMPLATE_DIR / "_seeds" # optional: for csv/license seed files
|
||||
|
||||
env = Environment(
|
||||
loader=FileSystemLoader(str(TEMPLATE_DIR)),
|
||||
autoescape=select_autoescape(enabled_extensions=()),
|
||||
trim_blocks=True,
|
||||
lstrip_blocks=True,
|
||||
)
|
||||
|
||||
def clean_dir(p: Path):
|
||||
if p.exists():
|
||||
for item in sorted(p.rglob("*"), reverse=True):
|
||||
if item.is_file():
|
||||
item.unlink()
|
||||
elif item.is_dir():
|
||||
item.rmdir()
|
||||
|
||||
def ensure_tree(scenario: str, hostname: str, esxi_host: str) -> Path:
|
||||
"""
|
||||
Returns the base path to .../staging/<scenario>/ and ensures the tree exists.
|
||||
"""
|
||||
base = STAGING / scenario
|
||||
(base / "host_vars" / hostname).mkdir(parents=True, exist_ok=True)
|
||||
(base / "host_vars" / esxi_host).mkdir(parents=True, exist_ok=True)
|
||||
return base
|
||||
|
||||
def render_to_file(template_name: str, context: Dict[str, Any], out_path: Path):
|
||||
print(f"[DEBUG] Rendering {out_path} with context: {context}")
|
||||
tmpl = env.get_template(template_name)
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
out_path.write_text(tmpl.render(**context))
|
||||
|
||||
def copy_seed(name: str, dest: Path):
|
||||
"""
|
||||
Copies a static seed file (e.g., impus.csv, supis.csv, license_vars.yaml)
|
||||
from templates/ansible_templates/_seeds/ if present; otherwise creates empty.
|
||||
"""
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
seed = STATIC_SEEDS / name
|
||||
if seed.exists():
|
||||
shutil.copyfile(seed, dest)
|
||||
else:
|
||||
dest.write_text("") # empty default
|
||||
1
site_info.json
Normal file
1
site_info.json
Normal file
@@ -0,0 +1 @@
|
||||
{"peers":[],"current_node":{"name":"JohnWayne","node":"pls@127.0.0.1","api_address":"192.168.86.54"},"unreachable_peers":[]}
|
||||
16
static/blueprints/00_simple_5G_only.json
Normal file
16
static/blueprints/00_simple_5G_only.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"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 } }
|
||||
],
|
||||
"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": "" } }
|
||||
]
|
||||
}
|
||||
}
|
||||
65
static/blueprints/08_all_in_one.json
Normal file
65
static/blueprints/08_all_in_one.json
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"layout": { "name": "preset" },
|
||||
"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": "dn", "label": "Data Network" }, "position": { "x": 800, "y": 240 } },
|
||||
{ "data": { "id": "mgmt", "label": "Management" }, "position": { "x": 450, "y": 0 } }
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"data": {
|
||||
"id": "e_ran_core",
|
||||
"source": "ran",
|
||||
"target": "core",
|
||||
"midLabel": "RAN Netw",
|
||||
"sourceLabel": "",
|
||||
"targetLabel": "S1/N2/N3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"id": "e_core_dn",
|
||||
"source": "core",
|
||||
"target": "dn",
|
||||
"midLabel": "DNN Netw",
|
||||
"sourceLabel": "SGi/N6 IP",
|
||||
"targetLabel": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"id": "e_mgmt_core",
|
||||
"source": "mgmt",
|
||||
"target": "core",
|
||||
"midLabel": "Mgmt Netw",
|
||||
"sourceLabel": "",
|
||||
"targetLabel": "Mgmt IP"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"styles": [
|
||||
{
|
||||
"selector": "#proxmox",
|
||||
"style": {
|
||||
"width": 340,
|
||||
"height": 180,
|
||||
"text-valign": "bottom",
|
||||
"text-margin-y": 20,
|
||||
"background-color": "#f8fafc",
|
||||
"border-color": "#94a3b8",
|
||||
"border-width": 2,
|
||||
"font-weight": "600"
|
||||
}
|
||||
},
|
||||
{
|
||||
"selector": "#core",
|
||||
"style": { "width": 140, "height": 70 }
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
static/images/favicon.ico
Normal file
BIN
static/images/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
0
static/images/favicon.png
Normal file
0
static/images/favicon.png
Normal file
BIN
static/images/hpe_logo_animated.gif
Normal file
BIN
static/images/hpe_logo_animated.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 490 KiB |
18
static/js/graph/initGraph.js
Normal file
18
static/js/graph/initGraph.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { baseStyles } from '/static/js/graph/style.js';
|
||||
|
||||
export function mountCy(container, graphJson) {
|
||||
const styles = Array.isArray(graphJson.styles)
|
||||
? [...baseStyles, ...graphJson.styles]
|
||||
: baseStyles;
|
||||
|
||||
const cy = cytoscape({
|
||||
container,
|
||||
style: styles,
|
||||
elements: graphJson.elements,
|
||||
layout: graphJson.layout || { name: 'preset' }
|
||||
});
|
||||
|
||||
cy.once('render', () => cy.fit(undefined, 40));
|
||||
window.addEventListener('resize', () => cy && cy.fit(undefined, 40));
|
||||
return cy;
|
||||
}
|
||||
68
static/js/graph/style.js
Normal file
68
static/js/graph/style.js
Normal file
@@ -0,0 +1,68 @@
|
||||
// /static/js/graph/style.js
|
||||
export const baseStyles = [
|
||||
{
|
||||
selector: 'node',
|
||||
style: {
|
||||
'shape': 'round-rectangle',
|
||||
'background-color': '#e8f0ff',
|
||||
'border-color': '#0ea5e9',
|
||||
'border-width': 2,
|
||||
'label': 'data(label)',
|
||||
'color': '#0f172a',
|
||||
'text-valign': 'center',
|
||||
'text-halign': 'center',
|
||||
'font-size': 12,
|
||||
'text-wrap': 'wrap',
|
||||
'text-max-width': 140,
|
||||
'padding': '8px',
|
||||
'width': 100,
|
||||
'height': 50
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: 'edge',
|
||||
style: {
|
||||
'line-color': '#64748b',
|
||||
'width': 2,
|
||||
'curve-style': 'straight',
|
||||
|
||||
// NEW: static “middle of edge” label
|
||||
'label': 'data(midLabel)',
|
||||
'text-rotation': 'autorotate',
|
||||
'text-margin-y': -8, // nudge mid-label off the line a bit
|
||||
|
||||
// keep your side labels for “where the IP sits”
|
||||
'source-label': 'data(sourceLabel)',
|
||||
'target-label': 'data(targetLabel)',
|
||||
'source-text-offset': 50,
|
||||
'target-text-offset': 50,
|
||||
|
||||
'font-size': 11,
|
||||
'color': '#334155',
|
||||
'text-background-color': '#ffffff',
|
||||
'text-background-opacity': 0.9,
|
||||
'text-background-shape': 'round-rectangle',
|
||||
'text-background-padding': 2,
|
||||
'text-outline-color': '#ffffff',
|
||||
'text-outline-width': 1
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: ':selected',
|
||||
style: { 'border-width': 3, 'border-color': '#22d3ee', 'line-color': '#22d3ee' }
|
||||
},
|
||||
|
||||
// Optional: make compound parents (like Proxmox) look like containers
|
||||
{
|
||||
selector: ':parent',
|
||||
style: {
|
||||
'background-color': '#f8fafc',
|
||||
'border-color': '#94a3b8',
|
||||
'border-width': 2,
|
||||
'padding': '12px',
|
||||
'text-valign': 'top',
|
||||
'text-halign': 'center',
|
||||
'font-weight': '600'
|
||||
}
|
||||
}
|
||||
];
|
||||
39
static/js/wizard/api.js
Normal file
39
static/js/wizard/api.js
Normal file
@@ -0,0 +1,39 @@
|
||||
// static/js/graph/wizard/api.js
|
||||
export async function setTarget(ip) {
|
||||
const r = await fetch('/api/host/target', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({ ip })
|
||||
});
|
||||
const j = await r.json();
|
||||
if (!r.ok || !j.ok) throw new Error(j.error || 'Failed to set target IP');
|
||||
return j;
|
||||
}
|
||||
|
||||
export async function getTarget() {
|
||||
const r = await fetch('/api/host/target');
|
||||
// returns { ip: "x.x.x.x" } or { ip: null }
|
||||
return r.ok ? r.json() : { ip: null };
|
||||
}
|
||||
|
||||
export async function bootstrapAccess() {
|
||||
const r = await fetch('/api/host/bootstrap_access', { method: 'POST' });
|
||||
const j = await r.json();
|
||||
if (!r.ok || !j.ok) throw new Error(j.error || 'Failed to bootstrap access');
|
||||
return j;
|
||||
}
|
||||
|
||||
// static/js/wizard/api.js
|
||||
export async function renderAnsible(payload) {
|
||||
const r = await fetch('/api/ansible/render', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const j = await r.json().catch(() => ({}));
|
||||
if (!r.ok || !j.ok) {
|
||||
const msg = j.error || `Render failed (HTTP ${r.status})`;
|
||||
throw new Error(msg);
|
||||
}
|
||||
return j; // { ok: true, staging: "ansible_workspace/staging" }
|
||||
}
|
||||
60
static/js/wizard/step0_target.js
Normal file
60
static/js/wizard/step0_target.js
Normal file
@@ -0,0 +1,60 @@
|
||||
// static/js/graph/wizard/step0_target.js
|
||||
import { setTarget, bootstrapAccess, getTarget } from './api.js';
|
||||
|
||||
export function mountTargetControls() {
|
||||
const row = document.querySelector('#target-host-row');
|
||||
if (!row) return;
|
||||
|
||||
// Use explicit IDs so we always hit the right elements
|
||||
const ipInput = document.getElementById('target-ip-input');
|
||||
const btn = document.getElementById('btn-enable-access');
|
||||
const badge = document.getElementById('access-badge');
|
||||
|
||||
if (!ipInput || !btn || !badge) return;
|
||||
|
||||
// Prefill: localStorage first, then try backend GET /api/host/target
|
||||
const saved = localStorage.getItem('targetHostIp');
|
||||
if (saved) ipInput.value = saved;
|
||||
getTarget()
|
||||
.then(({ ip }) => { if (ip && !saved) ipInput.value = ip; })
|
||||
.catch(() => { /* ignore if endpoint missing */ });
|
||||
|
||||
btn.addEventListener('click', async () => {
|
||||
const ip = (ipInput.value || '').trim();
|
||||
if (!ip) {
|
||||
badge.className = 'badge bg-warning';
|
||||
badge.textContent = 'Enter IP first';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
btn.disabled = true;
|
||||
const old = btn.textContent;
|
||||
btn.textContent = 'Enabling…';
|
||||
badge.className = 'badge bg-secondary';
|
||||
badge.textContent = 'Working';
|
||||
|
||||
await setTarget(ip); // POST /api/host/target
|
||||
await bootstrapAccess(); // POST /api/host/bootstrap_access (SSH+webconsole)
|
||||
// Persist for Stage 2 (and page reloads)
|
||||
localStorage.setItem('targetHostIp', ip);
|
||||
|
||||
badge.className = 'badge bg-success';
|
||||
badge.textContent = 'SSH & Webconsole Ready';
|
||||
btn.textContent = old;
|
||||
btn.disabled = false;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
badge.className = 'badge bg-danger';
|
||||
badge.textContent = 'Failed';
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Helper for other steps to read the latest target IP
|
||||
export function getTargetHostData() {
|
||||
const inputIp = (document.getElementById('target-ip-input')?.value || '').trim();
|
||||
const stored = localStorage.getItem('targetHostIp') || '';
|
||||
return { ip: inputIp || stored || '' };
|
||||
}
|
||||
77
static/js/wizard/step2_render.js
Normal file
77
static/js/wizard/step2_render.js
Normal file
@@ -0,0 +1,77 @@
|
||||
// static/js/graph/wizard/step2_render.js
|
||||
import { renderAnsible } from './api.js';
|
||||
import { getTargetHostData } from './step0_target.js';
|
||||
|
||||
export function mountStep2Render() {
|
||||
const btn = document.getElementById('btn-create-yaml');
|
||||
const badge = document.getElementById('yaml-badge');
|
||||
if (!btn) return;
|
||||
|
||||
btn.onclick = null; // Remove any previous handler
|
||||
btn.addEventListener('click', async () => {
|
||||
const origText = btn.textContent;
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Creating…';
|
||||
badge.className = 'badge bg-secondary';
|
||||
badge.textContent = 'Running';
|
||||
|
||||
try {
|
||||
const val = (id) => (document.getElementById(id)?.value || '').trim();
|
||||
// Target IP priority: localStorage → helper → DOM
|
||||
const lsIp = localStorage.getItem('targetHostIp') || '';
|
||||
const target = (typeof getTargetHostData === 'function' ? getTargetHostData() : {}) || {};
|
||||
const domIp = (document.querySelector('#target-host-row input')?.value || '').trim();
|
||||
const ansibleHostIp = (lsIp || target.ip || domIp || '').trim();
|
||||
|
||||
// Get eth0 info from Stage 1 UI fields
|
||||
const eth0Cidr = val('ip-core-mgmt');
|
||||
const eth0Gw = val('ip-core-mgmt-gw');
|
||||
|
||||
const payload = {
|
||||
hostname: val('network-name-input') || 'AIO-1',
|
||||
network_name: val('network-name-input'),
|
||||
plmn: val('plmn-input') || '315-010',
|
||||
dns: (val('dns-input') || '8.8.8.8').split(',').map(s => s.trim()).filter(Boolean),
|
||||
ntp: (val('ntp-input') || '0.pool.ntp.org, 1.pool.ntp.org').split(',').map(s => s.trim()).filter(Boolean),
|
||||
ran: { cidr: val('ip-core-ran'), gw: val('ip-core-ran-gw') },
|
||||
mgmt: { cidr: eth0Cidr, gw: eth0Gw },
|
||||
dn: {
|
||||
cidr: val('ip-core-dn'),
|
||||
gw: val('ip-core-dn-gw'),
|
||||
vlan: val('ip-core-dn-vlan') ? Number(val('ip-core-dn-vlan')) : undefined,
|
||||
ue_pool: val('ip-core-dn-uepool'),
|
||||
dnn: 'internet'
|
||||
},
|
||||
inventory_host: 'GBP08-AIO-1',
|
||||
esxi_host: 'ESXI-1',
|
||||
version: '25.1',
|
||||
ova_file: '/home/mjensen/OVA/HPE_ANW_P5G_Core-1.25.1.1-qemux86-64.ova',
|
||||
ansible_host_ip: ansibleHostIp
|
||||
};
|
||||
|
||||
const res = await renderAnsible(payload);
|
||||
|
||||
badge.className = 'badge bg-success';
|
||||
badge.textContent = 'Created';
|
||||
btn.textContent = origText;
|
||||
btn.disabled = false;
|
||||
|
||||
// Show YAML context in the UI for debug
|
||||
let debugDiv = document.getElementById('yaml-debug-info');
|
||||
if (!debugDiv) {
|
||||
debugDiv = document.createElement('div');
|
||||
debugDiv.id = 'yaml-debug-info';
|
||||
debugDiv.className = 'mt-3 alert alert-info';
|
||||
btn.parentNode.appendChild(debugDiv);
|
||||
}
|
||||
debugDiv.innerHTML = `<strong>YAML files created in:</strong> ${res.staging}<br><strong>Payload used:</strong><pre>${JSON.stringify(payload, null, 2)}</pre>`;
|
||||
|
||||
} catch (err) {
|
||||
badge.className = 'badge bg-danger';
|
||||
badge.textContent = 'Failed';
|
||||
btn.textContent = origText;
|
||||
btn.disabled = false;
|
||||
alert(`Failed to create YAML files:\n${err.message || err}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
24
static/js/wizard/step3_deploy.js
Normal file
24
static/js/wizard/step3_deploy.js
Normal file
@@ -0,0 +1,24 @@
|
||||
// static/js/wizard/step3_deploy.js
|
||||
export function mountStep3Deploy() {
|
||||
const btn = document.getElementById('btn-run-gaf');
|
||||
const outputDiv = document.getElementById('gaf-output');
|
||||
if (!btn || !outputDiv) return;
|
||||
|
||||
btn.onclick = null;
|
||||
btn.addEventListener('click', async () => {
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Running…';
|
||||
outputDiv.textContent = '';
|
||||
try {
|
||||
const res = await fetch('/api/ansible/deploy', { method: 'POST' });
|
||||
const text = await res.text();
|
||||
outputDiv.textContent = text;
|
||||
btn.textContent = 'Run';
|
||||
btn.disabled = false;
|
||||
} catch (err) {
|
||||
outputDiv.textContent = `Error: ${err.message || err}`;
|
||||
btn.textContent = 'Run';
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
11
static/js/wizard/steps.js
Normal file
11
static/js/wizard/steps.js
Normal file
@@ -0,0 +1,11 @@
|
||||
// static/js/graph/wizard/steps.js
|
||||
|
||||
import { mountTargetControls } from './step0_target.js';
|
||||
import { mountStep2Render } from './step2_render.js';
|
||||
import { mountStep3Deploy } from './step3_deploy.js';
|
||||
|
||||
export function mountWizard() {
|
||||
mountTargetControls();
|
||||
mountStep2Render();
|
||||
mountStep3Deploy();
|
||||
}
|
||||
24
system_info.json
Normal file
24
system_info.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"version": "1.25.1.1",
|
||||
"kernel": "6.6.62-athonet",
|
||||
"mem": {
|
||||
"free": "489500 kB",
|
||||
"total": "8128208 kB",
|
||||
"available": "3582980 kB"
|
||||
},
|
||||
"hostname": "JohnWayne",
|
||||
"vendor": "GenuineIntel",
|
||||
"arch": "x86_64",
|
||||
"product_name": "HPE Aruba Networking Private 5G Core",
|
||||
"bios_model": "pc-i440fx-9.2 CPU @ 2.0GHz",
|
||||
"bios_vendor": "QEMU",
|
||||
"cpu_model": "32-bit, 64-bit",
|
||||
"machine_id": "7ebd37b3c5a44ff7acafc84fa3af449d",
|
||||
"num_cpu": 4,
|
||||
"virtualization": "kvm",
|
||||
"target_host_ip": "100.93.1.100",
|
||||
"mgmt": {
|
||||
"cidr": "192.168.105.156/24",
|
||||
"gw": "192.168.105.1"
|
||||
}
|
||||
}
|
||||
68
templates/ansible_templates/aio_3gpp.yaml.j2
Normal file
68
templates/ansible_templates/aio_3gpp.yaml.j2
Normal file
@@ -0,0 +1,68 @@
|
||||
# 3GPP core identifiers / names
|
||||
mcc: "{{ mcc }}"
|
||||
mnc: "{{ mnc }}"
|
||||
full_network_name: "{{ network_name }}"
|
||||
short_network_name: "{{ network_name | replace(' ', '-') }}"
|
||||
|
||||
# AMF / GUAMI
|
||||
amf_name: "{{ amf_name | default('amf01.5gc.3gppnetwork.org') }}"
|
||||
guami:
|
||||
region: "{{ guami_region | default('02') }}"
|
||||
set: "{{ guami_set | default('003') }}"
|
||||
pointer: "{{ guami_pointer | default('000001') }}"
|
||||
|
||||
# MME (for 4G interop / S1)
|
||||
mme_name: "{{ mme_name | default('mme1') }}"
|
||||
mmegi: "{{ mmegi | default('0001') }}"
|
||||
mmec: "{{ mmec | default('01') }}"
|
||||
mme_cname: "{{ mme_cname | default('gw01.nodes') }}"
|
||||
|
||||
# DNS info
|
||||
epc_dns_zone_data:
|
||||
# Additional PLMNs to handle
|
||||
plmns:
|
||||
- { mcc: '999', mnc: '99' }
|
||||
- { mcc: '001', mnc: '01' }
|
||||
- { mcc: '{{ mcc }}', mnc: '{{ mnc }}' }
|
||||
|
||||
# SBI configuration
|
||||
sbi:
|
||||
interface: lo
|
||||
base_address: 127.0.1.1/24
|
||||
|
||||
# Transports configuration
|
||||
_ngc_ext_aio_transport:
|
||||
|
||||
# AIO local transports
|
||||
- action: set_local_transports
|
||||
params: {}
|
||||
|
||||
# RAN transports (use RAN IP)
|
||||
- action: override_amf_n2_transport
|
||||
params: { address: {{ ran.ip }}, vrf: RAN }
|
||||
- action: override_mme_transport
|
||||
params: { s1_address: {{ ran.ip }}, s1_vrf: RAN }
|
||||
|
||||
# UPF transports (N3 on RAN)
|
||||
- action: override_upf_transport
|
||||
params:
|
||||
n3_interface: eth1
|
||||
n3_address: {{ ran.ip }}
|
||||
n3_vrf: RAN
|
||||
|
||||
# DN/DNN (N6) with UE pool
|
||||
- action: add_n6_dnn
|
||||
params:
|
||||
n6_dnn: internet
|
||||
n6_vrf: DN_01
|
||||
n6_vlan: {{ dn.vlan }}
|
||||
n6_vrf_table: 511
|
||||
n6_interface: eth2
|
||||
n6_ip: {{ dn.cidr }}
|
||||
n6_gw: {{ dn.gw }}
|
||||
n6_upf_pools:
|
||||
- upf_route: {{ dn.ue_pool }}
|
||||
nssai: false
|
||||
n6_bgp:
|
||||
local_as: 65001
|
||||
peer_as: 65000
|
||||
10
templates/ansible_templates/aio_deploy.yaml.j2
Normal file
10
templates/ansible_templates/aio_deploy.yaml.j2
Normal file
@@ -0,0 +1,10 @@
|
||||
kind: ngcore-AIO
|
||||
nf_skip_list:
|
||||
- "aaa"
|
||||
- "chf"
|
||||
- "bmsc"
|
||||
- "dra"
|
||||
- "eir"
|
||||
version: '{{ version }}'
|
||||
ova_file: {{ ova_file }}
|
||||
report_services: {{ report_services | default(false) | lower }}
|
||||
34
templates/ansible_templates/aio_networking.yaml.j2
Normal file
34
templates/ansible_templates/aio_networking.yaml.j2
Normal file
@@ -0,0 +1,34 @@
|
||||
# --- Networking recipe ---
|
||||
net_recipe: generic_bgp
|
||||
|
||||
# --- OAM config ---
|
||||
oam_network:
|
||||
add_ansible_host_address: false
|
||||
addresses:
|
||||
- {{ mgmt.cidr }}
|
||||
gateway4: {{ mgmt.gw }}
|
||||
|
||||
# --- NTP ---
|
||||
ntp:
|
||||
{% for s in ntp %}
|
||||
- {{ s }}
|
||||
{% endfor %}
|
||||
|
||||
# --- VRF config ---
|
||||
_ngc_ext_aio_vrf:
|
||||
- action: net_add_vrf
|
||||
params: { name: RAN, table: 502 }
|
||||
- action: net_add_vrf
|
||||
params: { name: TELCO, table: 535 }
|
||||
|
||||
_ngc_ext_aio_net:
|
||||
# RAN interface
|
||||
- action: net_set_interface
|
||||
params:
|
||||
interface: eth1
|
||||
vrf: RAN
|
||||
addresses:
|
||||
- {{ ran.cidr }} # S1+N2+N3
|
||||
routes:
|
||||
- destination: 0.0.0.0/0
|
||||
gateway: {{ ran.gw }}
|
||||
13
templates/ansible_templates/aio_provisioning.yaml.j2
Normal file
13
templates/ansible_templates/aio_provisioning.yaml.j2
Normal file
@@ -0,0 +1,13 @@
|
||||
## UDM/UDR testing profile
|
||||
create_testing_profile:
|
||||
slices:
|
||||
- { sst: 1, sd: '000001' }
|
||||
- { sst: 1, sd: '' }
|
||||
dnns:
|
||||
- {{ dn.dnn }}
|
||||
plmns:
|
||||
- { mcc: '{{ mcc }}', mnc: '{{ mnc }}' }
|
||||
|
||||
# UDR Subscribers provisioning
|
||||
udr_provisioning:
|
||||
testing_profile_service_profile: "testing_profile"
|
||||
12
templates/ansible_templates/esxi.yaml.j2
Normal file
12
templates/ansible_templates/esxi.yaml.j2
Normal file
@@ -0,0 +1,12 @@
|
||||
vswitches:
|
||||
- vSwitchName: GAF_VSWITCH
|
||||
vSwitchNics: [vmnic4, vmnic5]
|
||||
vSwitchSecurity:
|
||||
forged_transmits: true
|
||||
mac_changes: true
|
||||
|
||||
portgroups:
|
||||
- { vSwitch: GAF_VSWITCH, vlanId: 501, vlanName: GAF_BP_501_OAM }
|
||||
- { vSwitch: GAF_VSWITCH, vlanId: {{ ran.vlan | default(502) }}, vlanName: GAF_BP_502_RAN }
|
||||
- { vSwitch: GAF_VSWITCH, vlanId: {{ dn.vlan }}, vlanName: DN_01 }
|
||||
- { vSwitch: GAF_VSWITCH, vlanId: 4095, vlanName: GAF_BP_T_510_515 }
|
||||
19
templates/ansible_templates/hosts.yaml.j2
Normal file
19
templates/ansible_templates/hosts.yaml.j2
Normal file
@@ -0,0 +1,19 @@
|
||||
all:
|
||||
hosts:
|
||||
{{ inventory_host }}:
|
||||
ansible_host: {{ ansible_host_ip }}
|
||||
children:
|
||||
ESXi:
|
||||
hosts:
|
||||
{{ esxi_host }}:
|
||||
VMs:
|
||||
children:
|
||||
_5GVMS:
|
||||
hosts:
|
||||
{{ inventory_host }}:
|
||||
_5GAIO:
|
||||
hosts:
|
||||
{{ inventory_host }}:
|
||||
vars:
|
||||
serialize: 2
|
||||
esxi_host: {{ esxi_host }}
|
||||
285
templates/index.html
Normal file
285
templates/index.html
Normal file
@@ -0,0 +1,285 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Core Network Tool</title>
|
||||
<link rel="icon" href="/static/images/favicon.ico">
|
||||
<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 { min-height: 100vh; }
|
||||
main { display: flex; flex-wrap: nowrap; height: 100vh; max-height: 100vh; overflow-x: auto; overflow-y: hidden; }
|
||||
.b-example-vr { flex-shrink: 0; width: 1.5rem; height: 100vh; background-color: rgba(0, 0, 0, .1); border: solid rgba(0, 0, 0, .15); border-width: 1px 0; }
|
||||
#results-output { background-color: #212529; color: #f8f9fa; border-radius: .25rem; min-height: 300px; font-family: monospace; }
|
||||
.table-hover tbody tr:hover { cursor: pointer; }
|
||||
.table-compact td,
|
||||
.table-compact th {
|
||||
padding-top: 0.2rem;
|
||||
padding-bottom: 0.2rem;
|
||||
}
|
||||
.accordion-button::after {
|
||||
filter: invert(1) grayscale(100%);
|
||||
}
|
||||
.network-card { cursor: pointer; }
|
||||
.network-card:hover { border-color: #0d6efd !important; }
|
||||
|
||||
.nav-pills .nav-link:not(.active) {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.sidebar-logo {
|
||||
height: 40px;
|
||||
width: auto;
|
||||
transition: transform 0.2s ease-in-out;
|
||||
}
|
||||
.sidebar-logo:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
.menu-header {
|
||||
padding: .5rem 1rem .25rem;
|
||||
font-size: .875rem;
|
||||
color: #6c757d;
|
||||
font-weight: 500;
|
||||
}
|
||||
input#host::placeholder {
|
||||
color: #6c757d;
|
||||
opacity: 1;
|
||||
}
|
||||
.tree-item { cursor: pointer; }
|
||||
.tree-item:hover { background-color: #495057; }
|
||||
.tree-item .bi { transition: transform 0.2s; }
|
||||
.tree-item.open > .bi-chevron-right { transform: rotate(90deg); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<main>
|
||||
<div class="d-flex flex-column flex-shrink-0 p-3 text-bg-dark" style="width: 280px;">
|
||||
<a href="/" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-white text-decoration-none">
|
||||
<img src="/static/images/hpe_logo_animated.gif" alt="HPE Logo" class="sidebar-logo">
|
||||
</a>
|
||||
<hr>
|
||||
<ul class="nav nav-pills flex-column mb-auto" id="menu">
|
||||
<li class="menu-header">Dashboard Functions</li>
|
||||
<li class="nav-item">
|
||||
<a href="{{ url_for('system_browser_page') }}" class="nav-link {% if active_page == 'system_browser' %}active{% endif %}" data-page-title="System Browser">
|
||||
<i class="bi bi-diagram-3-fill me-2"></i> System Browser
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="{{ url_for('vpn_status_page') }}" class="nav-link {% if active_page == 'vpn_status' %}active{% endif %}" data-page-title="m2000 Status">
|
||||
<i class="bi bi-shield-lock-fill me-2"></i> m2000 Status
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ url_for('network_config_page') }}" class="nav-link {% if active_page == 'network_config' %}active{% endif %}" data-page-title="Network Configuration">
|
||||
<i class="bi bi-hdd-network-fill me-2"></i> Network Config
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ url_for('tenants_page') }}" class="nav-link {% if active_page == 'tenants' %}active{% endif %}" data-page-title="Dashboard Tenant Management">
|
||||
<i class="bi bi-building-fill me-2"></i> Dashboard Tenant
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li><hr class="my-2"></li>
|
||||
|
||||
<li class="menu-header">ComcoCore Functions</li>
|
||||
<li>
|
||||
<a href="{{ url_for('hnk_page') }}" class="nav-link {% if active_page == 'hnk' %}active{% endif %}" data-page-title="Home Network Key (HNK) Management">
|
||||
<i class="bi bi-key-fill me-2"></i> Home Network Keys
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ url_for('network_clients_page') }}" class="nav-link {% if active_page == 'network_clients' %}active{% endif %}" data-page-title="Network Clients (SUPI)">
|
||||
<i class="bi bi-person-fill-gear me-2"></i> Network Clients
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="b-example-vr"></div>
|
||||
<div class="flex-grow-1 p-4 overflow-y-auto">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div class="modal fade" id="networkDetailModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="networkDetailModalLabel">Network Details</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<pre id="modalJsonOutput"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" defer></script>
|
||||
<script defer>
|
||||
const resultsOutput = document.getElementById('results-output');
|
||||
const spinner = document.getElementById('spinner');
|
||||
const networkDetailModal = document.getElementById('networkDetailModal');
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
||||
[...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
|
||||
checkVpnStatus();
|
||||
});
|
||||
|
||||
function formatHostIp(ipString) {
|
||||
if (!ipString) return '';
|
||||
if (ipString.includes(':') && !ipString.startsWith('[') && !ipString.endsWith(']')) {
|
||||
return `[${ipString}]`;
|
||||
}
|
||||
return ipString;
|
||||
}
|
||||
|
||||
async function apiCall(endpoint, body, clearResults = true) {
|
||||
spinner.classList.remove('d-none');
|
||||
if (clearResults) {
|
||||
resultsOutput.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) {
|
||||
if (result.error === 'Network Unreachable') throw new Error(result.message);
|
||||
throw new Error(result.error || 'Unknown server error');
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
alert(`Error: ${error.message}`);
|
||||
return null;
|
||||
} finally {
|
||||
spinner.classList.add('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusClass(status) {
|
||||
switch (status) {
|
||||
case 'DEPLOYED': case 'up': return 'bg-success';
|
||||
case 'ALL_HW_NOT_ONLINE': return 'bg-danger';
|
||||
case 'TO_DEPLOY': return 'bg-warning text-dark';
|
||||
default: return 'bg-secondary';
|
||||
}
|
||||
}
|
||||
|
||||
function initializeTooltips() {
|
||||
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
||||
[...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
|
||||
}
|
||||
|
||||
networkDetailModal.addEventListener('show.bs.modal', event => {
|
||||
const card = event.relatedTarget;
|
||||
const nodeName = card.getAttribute('data-node-name');
|
||||
const nodeDetails = JSON.parse(card.getAttribute('data-node-details'));
|
||||
const modalTitle = networkDetailModal.querySelector('.modal-title');
|
||||
const modalBody = networkDetailModal.querySelector('#modalJsonOutput');
|
||||
modalTitle.textContent = `Details for: ${nodeName}`;
|
||||
modalBody.textContent = JSON.stringify(nodeDetails, null, 2);
|
||||
});
|
||||
|
||||
const vpnToggles = document.querySelectorAll('.vpn-toggle');
|
||||
function updateVpnTogglesUI(activeVpn) {
|
||||
vpnToggles.forEach(toggle => {
|
||||
toggle.checked = (toggle.dataset.vpnName === activeVpn);
|
||||
toggle.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
async function checkVpnStatus() {
|
||||
try {
|
||||
const response = await fetch('/api/vpn/status');
|
||||
const data = await response.json();
|
||||
if (response.ok) updateVpnTogglesUI(data.active_vpn);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch VPN status:", error);
|
||||
}
|
||||
}
|
||||
|
||||
vpnToggles.forEach(toggle => {
|
||||
toggle.addEventListener('change', async (event) => {
|
||||
const vpnName = event.target.dataset.vpnName;
|
||||
const turnOn = event.target.checked;
|
||||
vpnToggles.forEach(t => t.disabled = true);
|
||||
try {
|
||||
const response = await fetch('/api/vpn/toggle', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ vpn_name: vpnName, state: turnOn })
|
||||
});
|
||||
const data = await response.json();
|
||||
if (!response.ok) {
|
||||
alert(`Error: ${data.error}`);
|
||||
}
|
||||
} catch (error) {
|
||||
alert(`Error: ${error.message}`);
|
||||
} finally {
|
||||
checkVpnStatus();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% block extra_scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
329
templates/layout.html
Normal file
329
templates/layout.html
Normal file
@@ -0,0 +1,329 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale-1">
|
||||
<title>{% block title %}Core Network Tool{% endblock %}</title>
|
||||
<link rel="icon" href="/static/images/favicon.ico">
|
||||
<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 { min-height: 100vh; }
|
||||
main { display: flex; flex-wrap: nowrap; height: 100vh; max-height: 100vh; overflow-x: auto; overflow-y: hidden; }
|
||||
.b-example-vr { flex-shrink: 0; width: 1.5rem; height: 100vh; background-color: rgba(0, 0, 0, .1); border: solid rgba(0, 0, 0, .15); border-width: 1px 0; }
|
||||
#results-output { background-color: #212529; color: #f8f9fa; border-radius: .25rem; min-height: 300px; font-family: monospace; }
|
||||
.table-hover tbody tr:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
.table-compact td,
|
||||
.table-compact th {
|
||||
padding-top: 0.2rem;
|
||||
padding-bottom: 0.2rem;
|
||||
}
|
||||
.accordion-button::after {
|
||||
filter: invert(1) grayscale(100%);
|
||||
}
|
||||
.network-card { cursor: pointer; }
|
||||
.network-card:hover { border-color: #0d6efd !important; }
|
||||
.nav-pills .nav-link:not(.active) {
|
||||
color: white;
|
||||
}
|
||||
.sidebar-logo {
|
||||
height: 40px;
|
||||
width: auto;
|
||||
transition: transform 0.2s ease-in-out;
|
||||
}
|
||||
.sidebar-logo:hover { transform: scale(1.05); }
|
||||
.menu-header {
|
||||
padding: .5rem 1rem .25rem;
|
||||
font-size: .875rem;
|
||||
color: #6c757d;
|
||||
font-weight: 500;
|
||||
}
|
||||
input#host::placeholder {
|
||||
color: #6c757d;
|
||||
opacity: 1;
|
||||
}
|
||||
.tree-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
.tree-item:hover { background-color: #495057; }
|
||||
.tree-item .bi {
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.tree-item.open > .bi-chevron-right { transform: rotate(90deg); }
|
||||
/* Scrollable sidebar */
|
||||
.sidebar-scroll {
|
||||
max-height: 100vh; /* full viewport height */
|
||||
overflow-y: auto; /* vertical scroll when needed */
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.sidebar-scroll {
|
||||
position: sticky;
|
||||
top: 0; /* adjust if navbar is fixed */
|
||||
}
|
||||
}
|
||||
|
||||
{% block extra_styles %}{% endblock %}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<main>
|
||||
<div class="d-flex flex-column flex-shrink-0 p-3 text-bg-dark vh-100" style="width: 280px;">
|
||||
<a href="/" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-white text-decoration-none">
|
||||
<img src="/static/images/hpe_logo_animated.gif" alt="HPE Logo" class="sidebar-logo">
|
||||
</a>
|
||||
<hr>
|
||||
{% set active_page = active_page|default('vpn_status') %}
|
||||
<div class="flex-grow-1 overflow-auto" style="min-height:0">
|
||||
<ul class="nav nav-pills flex-column mb-auto" id="menu">
|
||||
<li class="menu-header">Dashboard Functions</li>
|
||||
<li class="nav-item">
|
||||
<a href="{{ url_for('vpn_status_page') }}" class="nav-link {% if active_page == 'vpn_status' %}active{% endif %}">
|
||||
<i class="bi bi-shield-lock-fill me-2"></i> m2000 Status
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ url_for('network_config_page') }}" class="nav-link {% if active_page == 'network_config' %}active{% endif %}">
|
||||
<i class="bi bi-hdd-network-fill me-2"></i> Network Config
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ url_for('m2000_password_page') }}" class="nav-link {% if active_page == 'm2000_password' %}active{% endif %}">
|
||||
<i class="bi bi-asterisk me-2"></i> m2000 Password
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ url_for('tenants_page') }}" class="nav-link {% if active_page == 'tenants' %}active{% endif %}">
|
||||
<i class="bi bi-building-fill me-2"></i> Dashboard Tenant
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ url_for('users_page') }}" class="nav-link {% if active_page == 'users' %}active{% endif %}">
|
||||
<i class="bi bi-people-fill me-2"></i> Dashboard Users
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ url_for('m2000_reset_page') }}" class="nav-link {% if active_page == 'm2000_reset' %}active{% endif %}">
|
||||
<i class="bi bi-arrow-counterclockwise me-2"></i> m2000 Config Reset
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li><hr class="my-2"></li>
|
||||
|
||||
<li class="menu-header">ComcoCore Functions</li>
|
||||
<li>
|
||||
<a href="{{ url_for('system_browser_page') }}" class="nav-link {% if active_page == 'system_browser' %}active{% endif %}">
|
||||
<i class="bi bi-diagram-3-fill me-2"></i> System Browser
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ url_for('vpn_switcher_page') }}" class="nav-link {% if active_page == 'vpn_switcher' %}active{% endif %}">
|
||||
<i class="bi bi-arrow-repeat me-2"></i> VPN Switcher
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ url_for('hnk_page') }}" class="nav-link {% if active_page == 'hnk' %}active{% endif %}">
|
||||
<i class="bi bi-key-fill me-2"></i> Home Network Keys
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ url_for('network_clients_page') }}" class="nav-link {% if active_page == 'network_clients' %}active{% endif %}">
|
||||
<i class="bi bi-person-fill-gear me-2"></i> Network Clients
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ url_for('gaf_desk_page') }}" class="nav-link {% if active_page == 'gaf_desk' %}active{% endif %}">
|
||||
<i class="bi bi-layout-wtf me-2"></i> GAF Desk
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<hr>
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="b-example-vr"></div>
|
||||
<div class="flex-grow-1 p-4 overflow-y-auto">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div class="modal fade" id="networkDetailModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="networkDetailModalLabel">Network Details</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<pre id="modalJsonOutput"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" defer></script>
|
||||
<script defer>
|
||||
const resultsOutput = document.getElementById('results-output');
|
||||
const spinner = document.getElementById('spinner');
|
||||
const networkDetailModal = document.getElementById('networkDetailModal');
|
||||
|
||||
// ADDED: Reusable function to initialize tooltips
|
||||
function initializeTooltips() {
|
||||
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
||||
[...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initializeTooltips();
|
||||
checkVpnStatus();
|
||||
});
|
||||
|
||||
function formatHostIp(ipString) {
|
||||
if (!ipString) return '';
|
||||
if (ipString.includes(':') && !ipString.startsWith('[') && !ipString.endsWith(']')) {
|
||||
return `[${ipString}]`;
|
||||
}
|
||||
return ipString;
|
||||
}
|
||||
|
||||
async function apiCall(endpoint, body, clearResults = true) {
|
||||
spinner.classList.remove('d-none');
|
||||
if (clearResults) {
|
||||
resultsOutput.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) {
|
||||
if (result.error === 'Network Unreachable') throw new Error(result.message);
|
||||
throw new Error(result.error || 'Unknown server error');
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
alert(`Error: ${error.message}`);
|
||||
return null;
|
||||
} finally {
|
||||
spinner.classList.add('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusClass(status) {
|
||||
switch (status) {
|
||||
case 'DEPLOYED': case 'up': return 'bg-success';
|
||||
case 'ALL_HW_NOT_ONLINE': return 'bg-danger';
|
||||
case 'TO_DEPLOY': return 'bg-warning text-dark';
|
||||
default: return 'bg-secondary';
|
||||
}
|
||||
}
|
||||
|
||||
networkDetailModal.addEventListener('show.bs.modal', event => {
|
||||
const card = event.relatedTarget;
|
||||
const nodeName = card.getAttribute('data-node-name');
|
||||
const nodeDetails = JSON.parse(card.getAttribute('data-node-details'));
|
||||
const modalTitle = networkDetailModal.querySelector('.modal-title');
|
||||
const modalBody = networkDetailModal.querySelector('#modalJsonOutput');
|
||||
modalTitle.textContent = `Details for: ${nodeName}`;
|
||||
modalBody.textContent = JSON.stringify(nodeDetails, null, 2);
|
||||
});
|
||||
|
||||
const vpnToggles = document.querySelectorAll('.vpn-toggle');
|
||||
function updateVpnTogglesUI(activeVpn) {
|
||||
vpnToggles.forEach(toggle => {
|
||||
toggle.checked = (toggle.dataset.vpnName === activeVpn);
|
||||
toggle.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
async function checkVpnStatus() {
|
||||
try {
|
||||
const response = await fetch('/api/vpn/status');
|
||||
const data = await response.json();
|
||||
if (response.ok) updateVpnTogglesUI(data.active_vpn);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch VPN status:", error);
|
||||
}
|
||||
}
|
||||
|
||||
vpnToggles.forEach(toggle => {
|
||||
toggle.addEventListener('change', async (event) => {
|
||||
const vpnName = event.target.dataset.vpnName;
|
||||
const turnOn = event.target.checked;
|
||||
vpnToggles.forEach(t => t.disabled = true);
|
||||
try {
|
||||
const response = await fetch('/api/vpn/toggle', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ vpn_name: vpnName, state: turnOn })
|
||||
});
|
||||
const data = await response.json();
|
||||
if (!response.ok) {
|
||||
alert(`Error: ${data.error}`);
|
||||
}
|
||||
} catch (error) {
|
||||
alert(`Error: ${error.message}`);
|
||||
} finally {
|
||||
checkVpnStatus();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% block extra_scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
431
templates/pages/gaf_desk.html
Normal file
431
templates/pages/gaf_desk.html
Normal file
@@ -0,0 +1,431 @@
|
||||
{% extends "layout.html" %}
|
||||
{% set active_page = 'gaf_desk' %}
|
||||
{% block title %}GAF Desk - {{ super() }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 id="page-title" class="mb-0">GAF Desk: Configuration Generator</h2>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<label for="blueprint-select" class="form-label"><h5>1. Select a Blueprint</h5></label>
|
||||
<select class="form-select" id="blueprint-select">
|
||||
<option selected value="">Choose a blueprint...</option>
|
||||
<option value="00_simple_5G_only">00_simple_5G_only</option>
|
||||
<option value="01_single_site">01_single_site</option>
|
||||
<option value="02_high_availability">02_high_availability</option>
|
||||
<option value="03_distributed">03_distributed</option>
|
||||
<option value="04_high_availability_ipv6">04_high_availability_ipv6</option>
|
||||
<option value="05_high_availability_ospf">05_high_availability_ospf</option>
|
||||
<option value="06_upf_active_standby">06_upf_active_standby</option>
|
||||
<option value="07_high_availability_proxmox">07_high_availability_proxmox</option>
|
||||
<option value="08_all_in_one">08_all_in_one</option>
|
||||
<option value="10_4G_MVNO">10_4G_MVNO</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card my-4">
|
||||
<div class="card-header">
|
||||
<h5>2. Global Configuration</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<label for="network-name-input" class="form-label">Network Name</label>
|
||||
<input type="text" class="form-control" id="network-name-input" value="JohnWayne" readonly
|
||||
data-bs-toggle="tooltip" title="This is the hardcoded network name.">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label for="plmn-input" class="form-label">PLMN</label>
|
||||
<input type="text" class="form-control" id="plmn-input" value="315-010" readonly
|
||||
data-bs-toggle="tooltip" title="The hardcoded PLMN ID for this configuration.">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label for="dns-input" class="form-label">DNS</label>
|
||||
<input type="text" class="form-control" id="dns-input" value="8.8.8.8" readonly
|
||||
data-bs-toggle="tooltip" title="Primary DNS server. This value is fixed.">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label for="ntp-input" class="form-label">NTP</label>
|
||||
<input type="text" class="form-control" id="ntp-input" value="0.pool.ntp.org, 1.pool.ntp.org" readonly
|
||||
data-bs-toggle="tooltip" title="NTP servers for time synchronization.">
|
||||
</div>
|
||||
<div class="row g-3 mt-2">
|
||||
<!-- RAN -->
|
||||
<div class="col-md-4">
|
||||
<div class="mb-1"><span class="form-label d-block fw-semibold">RAN Network</span></div>
|
||||
|
||||
<input type="text" class="form-control" id="ip-core-ran"
|
||||
placeholder="Core IP addr in format 172.28.20.25/24"
|
||||
data-bs-toggle="tooltip"
|
||||
title="Enter the IP range for the connection between the Cell Site and the Core.">
|
||||
|
||||
<input type="text" class="form-control mt-2" id="ip-core-ran-gw"
|
||||
placeholder="RAN Gateway IP e.g. 172.28.20.1">
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="mb-1"><span class="form-label d-block fw-semibold">Management Network</span></div>
|
||||
|
||||
<input type="text" class="form-control" id="ip-core-mgmt"
|
||||
value="via DHCP" readonly
|
||||
placeholder="Will be set in Step 1"
|
||||
data-bs-toggle="tooltip"
|
||||
title="This IP will be set automatically based on the current DHCP address during prechecks.">
|
||||
|
||||
<input type="text" class="form-control mt-2" id="ip-core-mgmt-gw"
|
||||
value="via DHCP" readonly
|
||||
placeholder="Gateway will be set in Step 1"
|
||||
data-bs-toggle="tooltip"
|
||||
title="This gateway will be set automatically based on the current DHCP address during prechecks.">
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="mb-1"><span class="form-label d-block fw-semibold">Data Network</span></div>
|
||||
|
||||
<input type="text" class="form-control" id="ip-core-dn"
|
||||
placeholder="Core DN/APN IP addr in format 172.28.10.25/24"
|
||||
data-bs-toggle="tooltip"
|
||||
title="Enter the IP range for the connection between the Core and the Data Network.">
|
||||
|
||||
<input type="text" class="form-control mt-2" id="ip-core-dn-gw"
|
||||
placeholder="DN/APN Gateway IP e.g. 172.28.20.1">
|
||||
|
||||
<label for="vlan-core-dn" class="visually-hidden">Data Network VLAN</label>
|
||||
<input type="number" class="form-control mt-2" id="ip-core-dn-vlan"
|
||||
placeholder="VLAN (e.g. 200)" min="1" max="4094">
|
||||
|
||||
<label for="ue-ip-pool" class="visually-hidden">UE IP Pool (CIDR)</label>
|
||||
<input type="text" class="form-control mt-2" id="ip-core-dn-uepool"
|
||||
placeholder="UE IP Pool (e.g. 10.20.0.0/16)">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5>3. Blueprint Details</h5>
|
||||
|
||||
<div id="blueprint-diagram-area" class="mt-3">
|
||||
<div id="cy-5g" class="d-none" style="height:520px; border:1px solid #e5e7eb; border-radius:8px;"></div>
|
||||
|
||||
<div id="diagram-empty" class="text-muted text-center py-4" aria-live="polite">
|
||||
Please select a blueprint to see its diagram.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div id="deployment-wizard">
|
||||
<h5 class="mb-3">4. Deployment Wizard</h5>
|
||||
|
||||
<div class="progress mb-4" style="height: 25px;">
|
||||
<div class="progress-bar" role="progressbar" style="width: 0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">0%</div>
|
||||
</div>
|
||||
|
||||
<div class="accordion" id="deploymentAccordion">
|
||||
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="headingOne">
|
||||
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseOne">
|
||||
Stage 1: Prechecks
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapseOne" class="accordion-collapse collapse show" data-bs-parent="#deploymentAccordion">
|
||||
<div class="accordion-body">
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item" id="target-host-row">
|
||||
<div class="d-flex flex-wrap align-items-center gap-2">
|
||||
<strong class="me-2">Target host (VPN IP):</strong>
|
||||
<div class="input-group" style="max-width: 260px;">
|
||||
<span class="input-group-text">IPv4</span>
|
||||
<input id="target-ip-input" type="text" class="form-control" placeholder="10.x.x.x" inputmode="numeric" autocomplete="off">
|
||||
</div>
|
||||
<button id="btn-enable-access" class="btn btn-sm btn-primary">Enable Access</button>
|
||||
<span id="access-badge" class="badge bg-secondary ms-2">Pending</span>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap align-items-center gap-2 mt-2">
|
||||
<strong class="me-2">Retrieve eth0 network info:</strong>
|
||||
<button class="btn btn-sm btn-primary" id="btn-capture-oam">Run</button>
|
||||
<span class="badge bg-secondary ms-2" id="oam-badge">Pending</span>
|
||||
</div>
|
||||
<div class="small text-muted mt-2">
|
||||
Enables SSH & Webconsole (enable → enable-autostart → start).<br>
|
||||
Retrieves eth0 network info and configures static address.
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="headingTwo">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseTwo">
|
||||
Stage 2: Preparation
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapseTwo" class="accordion-collapse collapse" data-bs-parent="#deploymentAccordion">
|
||||
<div class="accordion-body">
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item" id="create-yaml-row">
|
||||
<div class="d-flex flex-wrap align-items-center gap-2">
|
||||
<strong class="me-2">Create YAML files:</strong>
|
||||
<button class="btn btn-sm btn-primary" id="btn-create-yaml">Create</button>
|
||||
<span class="badge bg-secondary ms-2" id="yaml-badge">Pending</span>
|
||||
</div>
|
||||
<div class="small text-muted mt-2">
|
||||
Generates YAML files for deployment based on the configuration above.
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="headingThree">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseThree">
|
||||
Stage 3: Execution
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapseThree" class="accordion-collapse collapse" data-bs-parent="#deploymentAccordion">
|
||||
<div class="accordion-body">
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item" id="run-gaf-row">
|
||||
<div class="d-flex flex-wrap align-items-center gap-2">
|
||||
<strong class="me-2">Run GAF:</strong>
|
||||
<button class="btn btn-sm btn-primary" id="btn-run-gaf">Run</button>
|
||||
</div>
|
||||
<div class="small text-muted mt-2">
|
||||
Executes the GAF deployment process.
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<pre id="gaf-output" style="background:#f8f9fa; border-radius:6px; padding:12px; max-height:300px; overflow:auto;"></pre>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="headingFour">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseFour">
|
||||
Stage 4: Postchecks
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapseFour" class="accordion-collapse collapse" data-bs-parent="#deploymentAccordion">
|
||||
<div class="accordion-body">
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item" id="post-backup-row">
|
||||
<div class="d-flex flex-wrap align-items-center gap-2">
|
||||
<strong class="me-2">Post-backup:</strong>
|
||||
<button class="btn btn-sm btn-primary" id="btn-post-backup" disabled>Run</button>
|
||||
</div>
|
||||
<div class="small text-muted mt-2">
|
||||
Performs post-deployment backup operations.
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script src="https://unpkg.com/cytoscape@3.28.0/dist/cytoscape.min.js"></script>
|
||||
<script src="https://unpkg.com/dagre@0.8.5/dist/dagre.min.js"></script>
|
||||
<script src="https://unpkg.com/cytoscape-dagre@2.5.0/cytoscape-dagre.js"></script>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
||||
[...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
import { mountCy } from '/static/js/graph/initGraph.js';
|
||||
import { mountWizard } from '/static/js/wizard/steps.js';
|
||||
import { mountStep3Deploy } from '/static/js/wizard/step3_deploy.js';
|
||||
mountWizard();
|
||||
mountStep3Deploy();
|
||||
|
||||
// === Recapture OAM (eth0) on demand ===
|
||||
|
||||
// Stage 3 deployment logic is now handled by static/js/wizard/step3_deploy.js
|
||||
async function recaptureOAM(host) {
|
||||
const oamBadge = document.getElementById('oam-badge');
|
||||
const mgmtInput = document.getElementById('ip-core-mgmt'); // the read-only field in "Management Network"
|
||||
|
||||
if (!host) {
|
||||
alert('Enter the Target host first (Stage 1).');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
oamBadge.textContent = 'Running…';
|
||||
oamBadge.className = 'badge bg-warning';
|
||||
|
||||
const res = await fetch('/api/local/eth0/capture', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ host })
|
||||
});
|
||||
const j = await res.json();
|
||||
|
||||
if (!res.ok || !j.ok) throw new Error(j.error || `HTTP ${res.status}`);
|
||||
|
||||
// Update UI with fresh values
|
||||
oamBadge.textContent = 'Captured';
|
||||
oamBadge.className = 'badge bg-success';
|
||||
if (mgmtInput) mgmtInput.value = j.cidr; // e.g. 192.168.86.55/24
|
||||
const mgmtGwInput = document.getElementById('ip-core-mgmt-gw');
|
||||
if (mgmtGwInput) mgmtGwInput.value = j.gw || '';
|
||||
|
||||
return j; // { cidr, gw, ok:true }
|
||||
} catch (e) {
|
||||
console.error('OAM capture failed:', e);
|
||||
oamBadge.textContent = 'Failed';
|
||||
oamBadge.className = 'badge bg-danger';
|
||||
alert(`OAM capture failed: ${e.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Wire the Stage 1 "Run" button to always re-capture
|
||||
document.getElementById('btn-capture-oam')?.addEventListener('click', async () => {
|
||||
const targetIp = document.querySelector('#target-host-row input')?.value?.trim();
|
||||
await recaptureOAM(targetIp);
|
||||
});
|
||||
|
||||
const blueprintSelect = document.getElementById('blueprint-select');
|
||||
const cyContainer = document.getElementById('cy-5g');
|
||||
const emptyMsg = document.getElementById('diagram-empty');
|
||||
|
||||
let cy = null;
|
||||
const cache = new Map();
|
||||
|
||||
function showEmpty() {
|
||||
if (cy) { cy.destroy(); cy = null; }
|
||||
cyContainer.classList.add('d-none');
|
||||
emptyMsg.textContent = 'Please select a blueprint to see its diagram.'; // reset
|
||||
emptyMsg.classList.remove('d-none');
|
||||
}
|
||||
|
||||
let dagreRegistered = false;
|
||||
|
||||
async function loadBlueprint(name) {
|
||||
if (!name) return showEmpty();
|
||||
|
||||
const url = `/static/blueprints/${name}.json`;
|
||||
try {
|
||||
const data = cache.has(url) ? cache.get(url) : await (await fetch(url, { cache: 'no-cache' })).json();
|
||||
cache.set(url, data);
|
||||
|
||||
emptyMsg.classList.add('d-none');
|
||||
cyContainer.classList.remove('d-none');
|
||||
|
||||
if (cy) { cy.destroy(); cy = null; }
|
||||
if (!dagreRegistered && window.cytoscapeDagre) {
|
||||
cytoscape.use(cytoscapeDagre);
|
||||
dagreRegistered = true;
|
||||
}
|
||||
|
||||
cy = mountCy(cyContainer, data);
|
||||
} catch (err) {
|
||||
console.error('Failed to load blueprint:', err);
|
||||
showEmpty();
|
||||
emptyMsg.textContent = 'Failed to load the selected blueprint.';
|
||||
}
|
||||
}
|
||||
|
||||
blueprintSelect.addEventListener('change', () => {
|
||||
const selectedValue = blueprintSelect.value;
|
||||
loadBlueprint(selectedValue);
|
||||
});
|
||||
|
||||
// Handle Create YAML button click (calls /api/ansible/render)
|
||||
document.getElementById('btn-create-yaml').addEventListener('click', async () => {
|
||||
const val = id => (document.getElementById(id)?.value || '').trim();
|
||||
|
||||
// Arrays
|
||||
const dns = (val('dns-input') || '8.8.8.8')
|
||||
.split(',')
|
||||
.map(s => s.trim())
|
||||
.filter(Boolean);
|
||||
const ntp = (val('ntp-input') || '0.pool.ntp.org, 1.pool.ntp.org')
|
||||
.split(',')
|
||||
.map(s => s.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
// DN VLAN as number (or undefined)
|
||||
const vlanStr = val('ip-core-dn-vlan');
|
||||
const vlanNum = vlanStr ? Number(vlanStr) : undefined;
|
||||
|
||||
// Target IP from Stage 1
|
||||
const targetIp = document.querySelector('#target-host-row input')?.value?.trim() || '';
|
||||
|
||||
// Always recapture just before render so we never use stale cache
|
||||
const cap = await recaptureOAM(targetIp); // <- requires the helper added earlier
|
||||
if (!cap) return; // capture failed; stop here
|
||||
|
||||
// Update the Management field from fresh capture (nice UX)
|
||||
const mgmtField = document.getElementById('ip-core-mgmt');
|
||||
if (mgmtField && cap.cidr) mgmtField.value = cap.cidr;
|
||||
|
||||
// Build payload; mgmt is determined server-side from the fresh capture
|
||||
const payload = {
|
||||
hostname: val('network-name-input') || 'AIO-1',
|
||||
network_name: val('network-name-input') || 'Network',
|
||||
plmn: val('plmn-input') || '315-010',
|
||||
dns,
|
||||
ntp,
|
||||
ran: { cidr: val('ip-core-ran'), gw: val('ip-core-ran-gw') },
|
||||
dn: {
|
||||
cidr: val('ip-core-dn'),
|
||||
gw: val('ip-core-dn-gw'),
|
||||
vlan: vlanNum,
|
||||
ue_pool: val('ip-core-dn-uepool'),
|
||||
dnn: 'internet'
|
||||
},
|
||||
inventory_host: 'GBP08-AIO-1',
|
||||
esxi_host: 'ESXI-1',
|
||||
version: '25.1',
|
||||
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
|
||||
ansible_host_ip: targetIp,
|
||||
force_oam_refresh: true // harmless hint; backend may ignore if not implemented
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/ansible/render', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const j = await res.json();
|
||||
if (!res.ok || !j.ok) throw new Error(j.error || `HTTP ${res.status}`);
|
||||
|
||||
document.getElementById('yaml-badge').textContent = 'Created';
|
||||
document.getElementById('yaml-badge').className = 'badge bg-success';
|
||||
alert(`YAML files created in: ${j.staging}`);
|
||||
} catch (err) {
|
||||
console.error('Error creating YAML:', err);
|
||||
document.getElementById('yaml-badge').textContent = 'Failed';
|
||||
document.getElementById('yaml-badge').className = 'badge bg-danger';
|
||||
}
|
||||
});
|
||||
|
||||
showEmpty();
|
||||
</script>
|
||||
{% endblock %}
|
||||
45
templates/pages/hnk.html
Normal file
45
templates/pages/hnk.html
Normal file
@@ -0,0 +1,45 @@
|
||||
{% extends "layout.html" %}
|
||||
{% set active_page = 'hnk' %}
|
||||
{% block title %}HNK Management - {{ super() }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 id="page-title" class="mb-0">Home Network Key (HNK) Management</h2>
|
||||
</div>
|
||||
<div class="row align-items-end">
|
||||
<div class="col-md-5" id="host-ip-wrapper">
|
||||
<label for="host" class="form-label">5GC Host IP</label>
|
||||
<input type="text" class="form-control" id="host" placeholder="IPv4 or IPv6 Address">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<i class="bi bi-info-circle" data-bs-toggle="tooltip" data-bs-placement="right" title="Enter the IPv4 or IPv6 address. IPv6 addresses are automatically enclosed in [ ] if needed."></i>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div id="hnk-view">
|
||||
<p>Retrieve a list of all Home Network Keys from the specified 5GC Host.</p>
|
||||
<button class="btn btn-primary" id="listHnkBtn">List HNKs</button>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<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>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script defer>
|
||||
document.getElementById('listHnkBtn').addEventListener('click', async () => {
|
||||
const rawHost = document.getElementById('host').value;
|
||||
if (!rawHost) {
|
||||
alert('Please enter a 5GC Host IP address.');
|
||||
return;
|
||||
}
|
||||
const host = formatHostIp(rawHost);
|
||||
const hnkData = await apiCall('/api/hnk/list', { host });
|
||||
if (hnkData) {
|
||||
resultsOutput.textContent = JSON.stringify(hnkData, null, 2);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
138
templates/pages/host_details.html
Normal file
138
templates/pages/host_details.html
Normal file
@@ -0,0 +1,138 @@
|
||||
{% extends "layout.html" %}
|
||||
{% set active_page = 'system_browser' %}
|
||||
{% block title %}Host Details - {{ super() }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 id="page-title" class="mb-0">Details for {{ details.system.hostname }}</h2>
|
||||
<a href="{{ url_for('system_browser_page') }}" class="btn btn-secondary">
|
||||
<i class="bi bi-arrow-left"></i> Back to System Browser
|
||||
</a>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
{% if details and not details.error %}
|
||||
<div class="row g-4">
|
||||
<div class="col-md-4">
|
||||
<h5>System Information</h5>
|
||||
<dl class="row">
|
||||
<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">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>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<h5>Site Information</h5>
|
||||
<dl class="row">
|
||||
<dt class="col-sm-4">Node Name</dt><dd class="col-sm-8">{{ details.site.current_node.name }}</dd>
|
||||
<dt class="col-sm-4">API Address</dt><dd class="col-sm-8">{{ details.site.current_node.api_address }}</dd>
|
||||
<dt class="col-sm-4">Site ID</dt><dd class="col-sm-8"><code>{{ details.licensed_host.siteid.value }}</code></dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<h5>License Information</h5>
|
||||
{% if details.license and details.license.license and details.license.license.license_params %}
|
||||
<dl class="row">
|
||||
<dt class="col-sm-5">Status</dt><dd class="col-sm-7">{{ details.license.state.str }}</dd>
|
||||
<dt class="col-sm-5">Start Date</dt><dd class="col-sm-7">{{ details.license.license.license_params.start_date_str }}</dd>
|
||||
<dt class="col-sm-5">Expire Date</dt><dd class="col-sm-7">{{ details.license.license.license_params.expire_date_str }}</dd>
|
||||
</dl>
|
||||
{% else %}
|
||||
<div class="alert alert-warning">No license information found.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<h5>Services Status</h5>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-dark table-hover table-compact">
|
||||
<thead id="services-table-head">
|
||||
<tr>
|
||||
<th data-sort="name">Service <i class="bi bi-arrow-down-up"></i></th>
|
||||
<th data-sort="version">Version <i class="bi bi-arrow-down-up"></i></th>
|
||||
<th data-sort="state">Status <i class="bi bi-arrow-down-up"></i></th>
|
||||
<th data-sort="license_id">License ID <i class="bi bi-arrow-down-up"></i></th>
|
||||
<th data-sort="start_date">Start Date <i class="bi bi-arrow-down-up"></i></th>
|
||||
<th data-sort="expire_date">Expire Date <i class="bi bi-arrow-down-up"></i></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="services-table-body">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<div class="alert alert-danger">Could not load details for this host. Please check the server logs.</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script defer>
|
||||
const allServices = {{ details.services|tojson }};
|
||||
let currentSort = { column: 'name', direction: 'asc' };
|
||||
|
||||
function renderServiceTable(services) {
|
||||
const tableBody = document.getElementById('services-table-body');
|
||||
tableBody.innerHTML = '';
|
||||
|
||||
services.forEach(service => {
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `
|
||||
<td>${service.name}</td>
|
||||
<td>${service.version}</td>
|
||||
<td><span class="badge ${service.state === 'started' ? 'bg-success' : 'bg-danger'}">${service.state}</span></td>
|
||||
<td><code>${service.license ? service.license.license_id : 'N/A'}</code></td>
|
||||
<td>${service.license ? service.license.license.license_params.start_date_str : 'N/A'}</td>
|
||||
<td>${service.license ? service.license.license.license_params.expire_date_str : 'N/A'}</td>
|
||||
`;
|
||||
tableBody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
function sortAndRender() {
|
||||
allServices.sort((a, b) => {
|
||||
let valA, valB;
|
||||
if (currentSort.column === 'license_id') {
|
||||
valA = a.license ? a.license.license_id : '';
|
||||
valB = b.license ? b.license.license_id : '';
|
||||
} else if (currentSort.column === 'start_date' || currentSort.column === 'expire_date') {
|
||||
valA = a.license ? a.license.license.license_params[currentSort.column + '_str'] : '';
|
||||
valB = b.license ? b.license.license.license_params[currentSort.column + '_str'] : '';
|
||||
} else {
|
||||
valA = a[currentSort.column] || '';
|
||||
valB = b[currentSort.column] || '';
|
||||
}
|
||||
|
||||
if (valA < valB) return currentSort.direction === 'asc' ? -1 : 1;
|
||||
if (valA > valB) return currentSort.direction === 'asc' ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
renderServiceTable(allServices);
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
sortAndRender();
|
||||
});
|
||||
|
||||
document.getElementById('services-table-head').addEventListener('click', (event) => {
|
||||
const header = event.target.closest('th[data-sort]');
|
||||
if (header) {
|
||||
const sortColumn = header.dataset.sort;
|
||||
if (currentSort.column === sortColumn) {
|
||||
currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
currentSort.column = sortColumn;
|
||||
currentSort.direction = 'asc';
|
||||
}
|
||||
sortAndRender();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
70
templates/pages/m2000_config_reset.html
Normal file
70
templates/pages/m2000_config_reset.html
Normal file
@@ -0,0 +1,70 @@
|
||||
{% extends "layout.html" %}
|
||||
{% set active_page = 'm2000_reset' %}
|
||||
{% block title %}m2000 Config Reset - {{ super() }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 id="page-title" class="mb-0">m2000 Configuration Reset</h2>
|
||||
</div>
|
||||
<hr>
|
||||
<div id="reset-view">
|
||||
<p>Enter a base IPv6 address to reset the configuration for AMF, UPF, SMF, SGWC, MME, and PCF services on the derived hosts (ending in ':a' and ':b').</p>
|
||||
<div class="row align-items-end g-3">
|
||||
<div class="col-md-6">
|
||||
<label for="base-ip-input" class="form-label">Base IPv6 Address</label>
|
||||
<input type="text" class="form-control" id="base-ip-input" placeholder="e.g., fd14:6666::1a:0">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button class="btn btn-danger" id="resetConfigBtn">Reset Configuration</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<h4>Result</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>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script defer>
|
||||
document.getElementById('resetConfigBtn').addEventListener('click', async () => {
|
||||
const baseIp = document.getElementById('base-ip-input').value;
|
||||
if (!baseIp) {
|
||||
alert('Please enter a base IPv6 address.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm('Are you sure you want to factory reset the configuration for 6 services on 2 hosts? This action cannot be undone.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const results = await apiCall('/api/m2000/reset-config', { base_ip: baseIp });
|
||||
if (results) {
|
||||
let tableHtml = `<table class="table table-dark table-sm table-compact">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Host</th>
|
||||
<th>Service</th>
|
||||
<th>Status</th>
|
||||
<th>Message</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>`;
|
||||
|
||||
results.forEach(result => {
|
||||
const statusClass = result.status === 'Success' ? 'bg-success' : 'bg-danger';
|
||||
tableHtml += `<tr>
|
||||
<td>${result.host}</td>
|
||||
<td>${result.service}</td>
|
||||
<td><span class="badge ${statusClass}">${result.status}</span></td>
|
||||
<td>${result.message}</td>
|
||||
</tr>`;
|
||||
});
|
||||
|
||||
tableHtml += '</tbody></table>';
|
||||
resultsOutput.innerHTML = tableHtml;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
67
templates/pages/m2000_password.html
Normal file
67
templates/pages/m2000_password.html
Normal file
@@ -0,0 +1,67 @@
|
||||
{% extends "layout.html" %}
|
||||
{% set active_page = 'm2000_password' %}
|
||||
{% block title %}m2000 Password Generator - {{ super() }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 id="page-title" class="mb-0">m2000 Password Generator</h2>
|
||||
</div>
|
||||
<hr>
|
||||
<div id="password-view">
|
||||
<p>Enter an m2000 serial number to generate its password.</p>
|
||||
<div class="row align-items-end g-3">
|
||||
<div class="col-md-4">
|
||||
<label for="serial-input" class="form-label">Serial Number</label>
|
||||
<input type="text" class="form-control" id="serial-input">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button class="btn btn-primary" id="getPasswordBtn">Generate Password</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<h4>Result</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>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script defer>
|
||||
const serialInput = document.getElementById('serial-input');
|
||||
const getPasswordBtn = document.getElementById('getPasswordBtn');
|
||||
|
||||
async function generatePassword() {
|
||||
const serial = serialInput.value;
|
||||
if (!serial) {
|
||||
alert('Please enter a serial number.');
|
||||
return;
|
||||
}
|
||||
|
||||
// CHANGED: Update the URL in the browser's history
|
||||
history.pushState({}, '', `/m2000psw?serial=${serial}`);
|
||||
|
||||
const result = await apiCall('/api/m2000/get-password', { serial });
|
||||
if (result) {
|
||||
resultsOutput.innerHTML = `
|
||||
<dl class="row">
|
||||
<dt class="col-sm-3">Serial Number:</dt>
|
||||
<dd class="col-sm-9">${result.serial}</dd>
|
||||
<dt class="col-sm-3">Derived Password:</dt>
|
||||
<dd class="col-sm-9"><code>${result.password}</code></dd>
|
||||
</dl>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
getPasswordBtn.addEventListener('click', generatePassword);
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const serialFromUrl = '{{ serial_from_url|default("", True) }}';
|
||||
if (serialFromUrl) {
|
||||
serialInput.value = serialFromUrl;
|
||||
getPasswordBtn.click();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
45
templates/pages/network_clients.html
Normal file
45
templates/pages/network_clients.html
Normal file
@@ -0,0 +1,45 @@
|
||||
{% extends "layout.html" %}
|
||||
{% set active_page = 'network_clients' %}
|
||||
{% block title %}Network Clients - {{ super() }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 id="page-title" class="mb-0">Network Clients (SUPI)</h2>
|
||||
</div>
|
||||
<div class="row align-items-end">
|
||||
<div class="col-md-5" id="host-ip-wrapper">
|
||||
<label for="host" class="form-label">5GC Host IP</label>
|
||||
<input type="text" class="form-control" id="host" placeholder="IPv4 or IPv6 Address">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<i class="bi bi-info-circle" data-bs-toggle="tooltip" data-bs-placement="right" title="Enter the IPv4 or IPv6 address. IPv6 addresses are automatically enclosed in [ ] if needed."></i>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div id="subscriber-view">
|
||||
<p>Retrieve a list of all Network Clients (SUPIs) from the specified 5GC Host.</p>
|
||||
<button class="btn btn-primary" id="listSupiBtn">List Network Clients</button>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<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>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script defer>
|
||||
document.getElementById('listSupiBtn').addEventListener('click', async () => {
|
||||
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);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
189
templates/pages/network_config.html
Normal file
189
templates/pages/network_config.html
Normal file
@@ -0,0 +1,189 @@
|
||||
{% extends "layout.html" %}
|
||||
{% set active_page = 'network_config' %}
|
||||
{% block title %}Network Config - {{ super() }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 id="page-title" class="mb-0">Network Configuration</h2>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div id="netconfig-view">
|
||||
<p>Retrieve a summary of network nodes from the selected dashboard. Click on any node to view its full configuration.</p>
|
||||
<button class="btn btn-primary" id="getConfigBtn">Get Network Config</button>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div id="details-pane" class="d-none">
|
||||
<hr>
|
||||
<h4>Selected Network Details</h4>
|
||||
<div class="row g-4">
|
||||
<div class="col-md-6">
|
||||
<h5>General Info</h5>
|
||||
<dl class="row">
|
||||
<dt class="col-sm-4">Name</dt>
|
||||
<dd class="col-sm-8" id="detail-name"></dd>
|
||||
<dt class="col-sm-4">Tenant</dt>
|
||||
<dd class="col-sm-8" id="detail-tenant"></dd>
|
||||
<dt class="col-sm-4">Status</dt>
|
||||
<dd class="col-sm-8" id="detail-status"></dd>
|
||||
<dt class="col-sm-4">Reachable</dt>
|
||||
<dd class="col-sm-8" id="detail-reachable"></dd>
|
||||
<dt class="col-sm-4">Serials</dt>
|
||||
<dd class="col-sm-8" id="detail-serials"></dd>
|
||||
<dt class="col-sm-4">Radios</dt>
|
||||
<dd class="col-sm-8">
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" id="detail-radios-input" min="0">
|
||||
<button class="btn btn-success" id="updateRadiosBtn">Update</button>
|
||||
</div>
|
||||
</dd>
|
||||
</dl>
|
||||
<h5>RAN Addressing</h5>
|
||||
<dl class="row">
|
||||
<dt class="col-sm-4">Gateway</dt>
|
||||
<dd class="col-sm-8" id="detail-ran-gateway"></dd>
|
||||
<dt class="col-sm-4">Subnet</dt>
|
||||
<dd class="col-sm-8" id="detail-ran-subnet"></dd>
|
||||
<dt class="col-sm-4">Radio Subnets</dt>
|
||||
<dd class="col-sm-8" id="detail-ran-radiosubnets"></dd>
|
||||
<dt class="col-sm-4">VLAN</dt>
|
||||
<dd class="col-sm-8" id="detail-ran-vlan"></dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h5>Data Network (DNN) List</h5>
|
||||
<div id="detail-dnn-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script defer>
|
||||
const detailsPane = document.getElementById('details-pane');
|
||||
let fullNodeConfig = []; // Store the full config data
|
||||
|
||||
function populateDetailsPane(node) {
|
||||
detailsPane.dataset.networkId = node.id;
|
||||
document.getElementById('detail-name').textContent = node.name || 'N/A';
|
||||
document.getElementById('detail-tenant').textContent = node.tenant?.name || 'N/A';
|
||||
document.getElementById('detail-status').innerHTML = `<span class="badge ${getStatusClass(node.status)}">${node.status}</span>`;
|
||||
document.getElementById('detail-reachable').innerHTML = `<span class="badge ${node.network_reachable ? 'bg-success' : 'bg-danger'}">${node.network_reachable ? 'Reachable' : 'Unreachable'}</span>`;
|
||||
document.getElementById('detail-serials').textContent = node.info?.hardware?.map(hw => hw.serial).join(', ') || 'N/A';
|
||||
document.getElementById('detail-radios-input').value = node.info?.radio_pool?.[0]?.num_of_radios ?? 0;
|
||||
const ran = node.addressing?.ran;
|
||||
document.getElementById('detail-ran-gateway').textContent = ran?.gateway || 'N/A';
|
||||
document.getElementById('detail-ran-subnet').textContent = ran?.subnet || 'N/A';
|
||||
document.getElementById('detail-ran-radiosubnets').textContent = ran?.radio_subnets?.join(', ') || 'N/A';
|
||||
document.getElementById('detail-ran-vlan').textContent = ran?.vlan || 'N/A';
|
||||
const dnnListContainer = document.getElementById('detail-dnn-list');
|
||||
dnnListContainer.innerHTML = '';
|
||||
const dnnList = node.addressing?.dn_list || [];
|
||||
if (dnnList.length > 0) {
|
||||
dnnList.forEach((dnn, index) => {
|
||||
const dnnHtml = `
|
||||
<div class="mb-3">
|
||||
<h6>DNN #${index + 1}: ${dnn.dnn}</h6>
|
||||
<dl class="row">
|
||||
<dt class="col-sm-4">Gateway</dt><dd class="col-sm-8">${dnn.gateway || 'N/A'}</dd>
|
||||
<dt class="col-sm-4">Subnet</dt><dd class="col-sm-8">${dnn.subnet || 'N/A'}</dd>
|
||||
<dt class="col-sm-4">IP Pools</dt><dd class="col-sm-8">${dnn.ip_pools?.join(', ') || 'N/A'}</dd>
|
||||
<dt class="col-sm-4">DNS</dt><dd class="col-sm-8">${dnn.dns?.join(', ') || 'N/A'}</dd>
|
||||
<dt class="col-sm-4">VLAN</dt><dd class="col-sm-8">${dnn.vlan || 'N/A'}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
`;
|
||||
dnnListContainer.innerHTML += dnnHtml;
|
||||
});
|
||||
} else {
|
||||
dnnListContainer.textContent = 'No Data Networks configured.';
|
||||
}
|
||||
detailsPane.classList.remove('d-none');
|
||||
}
|
||||
|
||||
document.getElementById('getConfigBtn').addEventListener('click', async () => {
|
||||
detailsPane.classList.add('d-none');
|
||||
const dashboard = document.getElementById('dashboard-select').value;
|
||||
const configData = await apiCall('/api/network/get-config', { dashboard });
|
||||
if (configData && Array.isArray(configData.items)) {
|
||||
fullNodeConfig = configData.items; // Store the full data
|
||||
let cardsHtml = '<div class="row g-3">';
|
||||
fullNodeConfig.forEach((node, index) => {
|
||||
const name = node.name || 'Unnamed Node';
|
||||
cardsHtml += `
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="card h-100 network-card" data-node-index="${index}">
|
||||
<div class="card-header"><strong>${name}</strong></div>
|
||||
<div class="card-body">
|
||||
<p class="card-text mb-1"><small>Tenant: ${node.tenant?.name || 'N/A'}</small></p>
|
||||
<p class="card-text mb-1"><small>Serials: ${node.info?.hardware?.map(hw => hw.serial).join(', ') || 'N/A'}</small></p>
|
||||
<p class="card-text"><small>Radios: ${node.info?.radio_pool?.[0]?.num_of_radios ?? 'N/A'}</small></p>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-between align-items-center">
|
||||
<span class="badge ${getStatusClass(node.status)}">${node.status}</span>
|
||||
<span class="badge ${node.network_reachable ? 'bg-success' : 'bg-danger'}">
|
||||
${node.network_reachable ? 'Reachable' : 'Unreachable'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
cardsHtml += '</div>';
|
||||
resultsOutput.innerHTML = cardsHtml;
|
||||
} else {
|
||||
resultsOutput.innerHTML = `<div class="alert alert-info">Could not find network items in the response.</div>`;
|
||||
}
|
||||
});
|
||||
|
||||
resultsOutput.addEventListener('click', (event) => {
|
||||
const card = event.target.closest('.network-card');
|
||||
if (card) {
|
||||
document.querySelectorAll('.network-card.active').forEach(c => c.classList.remove('active'));
|
||||
card.classList.add('active');
|
||||
const nodeIndex = card.dataset.nodeIndex;
|
||||
const selectedNode = fullNodeConfig[nodeIndex];
|
||||
if (selectedNode) {
|
||||
populateDetailsPane(selectedNode);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('updateRadiosBtn').addEventListener('click', async () => {
|
||||
const networkId = detailsPane.dataset.networkId;
|
||||
const newCount = document.getElementById('detail-radios-input').value;
|
||||
const dashboard = document.getElementById('dashboard-select').value;
|
||||
|
||||
if (!networkId) {
|
||||
alert('No network selected.');
|
||||
return;
|
||||
}
|
||||
const node = fullNodeConfig.find(n => n.id === networkId);
|
||||
const operation = (node && node.info && node.info.radio_pool && node.info.radio_pool.length > 0) ? 'replace' : 'add';
|
||||
|
||||
const result = await apiCall('/api/network/update-radios', { dashboard, network_id: networkId, new_count: newCount, operation });
|
||||
if (result) {
|
||||
document.getElementById('getConfigBtn').click();
|
||||
detailsPane.classList.add('d-none');
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
153
templates/pages/system_browser.html
Normal file
153
templates/pages/system_browser.html
Normal file
@@ -0,0 +1,153 @@
|
||||
{% extends "layout.html" %}
|
||||
{% set active_page = 'system_browser' %}
|
||||
{% block title %}System Browser - {{ super() }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 id="page-title" class="mb-0">System Browser</h2>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<div class="row g-3 my-3 align-items-end">
|
||||
<div class="col-md-6">
|
||||
<label for="customer-filter" class="form-label">Filter by Customer</label>
|
||||
<select id="customer-filter" class="form-select"></select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="search-input" class="form-label">Search Results</label>
|
||||
<input type="text" id="search-input" class="form-control" placeholder="Search any column...">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<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>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script defer>
|
||||
let fullBrowserData = [];
|
||||
let currentSort = { column: 'customer_id', direction: 'asc' };
|
||||
|
||||
const searchInput = document.getElementById('search-input');
|
||||
const customerFilter = document.getElementById('customer-filter');
|
||||
|
||||
function renderTable(data) {
|
||||
let tableHtml = `<table class="table table-dark table-hover table-sm table-compact">
|
||||
<thead>
|
||||
<tr>
|
||||
<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>
|
||||
</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}"
|
||||
data-public-ip="${client.public_ip}"
|
||||
data-connected-since="${client.connected_since}"
|
||||
data-customer-name="${client.customer_name}"
|
||||
data-common-name="${client.common_name}"
|
||||
style="cursor: pointer;">
|
||||
<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>
|
||||
</tr>`;
|
||||
});
|
||||
|
||||
tableHtml += '</tbody></table>';
|
||||
resultsOutput.innerHTML = tableHtml;
|
||||
}
|
||||
|
||||
function populateCustomerFilter(data) {
|
||||
const customers = [...new Set(data.map(item => item.customer_name))].sort();
|
||||
customerFilter.innerHTML = '<option value="">All Customers</option>';
|
||||
customers.forEach(customer => {
|
||||
const option = document.createElement('option');
|
||||
option.value = customer;
|
||||
option.textContent = customer;
|
||||
customerFilter.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
function applyFiltersAndSort() {
|
||||
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 =>
|
||||
String(value).toLowerCase().includes(searchTerm)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
filteredData.sort((a, b) => {
|
||||
const valA = a[currentSort.column] || '';
|
||||
const valB = b[currentSort.column] || '';
|
||||
if (valA < valB) return currentSort.direction === 'asc' ? -1 : 1;
|
||||
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', {});
|
||||
if (browserData) {
|
||||
fullBrowserData = browserData;
|
||||
populateCustomerFilter(fullBrowserData);
|
||||
applyFiltersAndSort();
|
||||
}
|
||||
});
|
||||
|
||||
searchInput.addEventListener('input', applyFiltersAndSort);
|
||||
customerFilter.addEventListener('change', applyFiltersAndSort);
|
||||
|
||||
// RESTORED: The full click handler for the results area
|
||||
resultsOutput.addEventListener('click', (event) => {
|
||||
const header = event.target.closest('th[data-sort]');
|
||||
if (header) {
|
||||
const sortColumn = header.dataset.sort;
|
||||
if (currentSort.column === sortColumn) {
|
||||
currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
currentSort.column = sortColumn;
|
||||
currentSort.direction = 'asc';
|
||||
}
|
||||
applyFiltersAndSort();
|
||||
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}`;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
126
templates/pages/tenants.html
Normal file
126
templates/pages/tenants.html
Normal file
@@ -0,0 +1,126 @@
|
||||
{% extends "layout.html" %}
|
||||
{% set active_page = 'tenants' %}
|
||||
{% block title %}Dashboard Tenants - {{ super() }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 id="page-title" class="mb-0">Dashboard Tenant Management</h2>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div id="tenant-view">
|
||||
<p>Retrieve the tenant hierarchy from the selected dashboard. Click on any item to expand it.</p>
|
||||
<button class="btn btn-primary" id="getTenantsBtn">Get Tenants</button>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<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>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script defer>
|
||||
document.getElementById('getTenantsBtn').addEventListener('click', async () => {
|
||||
const dashboard = document.getElementById('dashboard-select').value;
|
||||
const tenants = await apiCall('/api/tenants/list', { dashboard });
|
||||
if (tenants) {
|
||||
resultsOutput.innerHTML = '<ul class="list-group"></ul>';
|
||||
const listContainer = resultsOutput.querySelector('.list-group');
|
||||
tenants.forEach(tenant => {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'list-group-item list-group-item-dark tree-item';
|
||||
li.dataset.level = 'tenant';
|
||||
li.dataset.id = tenant.id;
|
||||
li.dataset.loaded = 'false';
|
||||
li.innerHTML = `<i class="bi bi-chevron-right me-2"></i> <strong>${tenant.name}</strong> <small class="text-muted">(${tenant.type})</small>`;
|
||||
listContainer.appendChild(li);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
resultsOutput.addEventListener('click', async (event) => {
|
||||
const item = event.target.closest('.tree-item');
|
||||
if (item) {
|
||||
const level = item.dataset.level;
|
||||
const id = item.dataset.id;
|
||||
const isLoaded = item.dataset.loaded === 'true';
|
||||
const isOpen = item.classList.contains('open');
|
||||
const dashboard = document.getElementById('dashboard-select').value;
|
||||
|
||||
if (isLoaded) {
|
||||
const childrenContainer = item.nextElementSibling;
|
||||
if (childrenContainer && childrenContainer.classList.contains('nested-group')) {
|
||||
childrenContainer.classList.toggle('d-none');
|
||||
item.classList.toggle('open', !isOpen);
|
||||
item.querySelector('.bi-chevron-right')?.classList.toggle('open');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
item.dataset.loaded = 'true';
|
||||
item.classList.add('open');
|
||||
item.querySelector('.bi-chevron-right')?.classList.add('open');
|
||||
const spinnerSpan = document.createElement('span');
|
||||
spinnerSpan.className = 'spinner-border spinner-border-sm ms-2';
|
||||
item.appendChild(spinnerSpan);
|
||||
|
||||
let childrenData = [];
|
||||
if (level === 'tenant') {
|
||||
// CHANGED: Added 'false' to prevent clearing the results
|
||||
childrenData = await apiCall('/api/plmns/list', { dashboard, tenant_id: id }, false);
|
||||
renderChildren(item, childrenData, 'plmn');
|
||||
} else if (level === 'plmn') {
|
||||
const tenantId = item.dataset.tenantId;
|
||||
// CHANGED: Added 'false' to prevent clearing the results
|
||||
childrenData = await apiCall('/api/hnks/list/by-plmn', { dashboard, tenant_id: tenantId, plmn_id: id }, false);
|
||||
renderChildren(item, childrenData, 'hnk');
|
||||
}
|
||||
|
||||
spinnerSpan.remove();
|
||||
}
|
||||
});
|
||||
|
||||
function renderChildren(parentItem, children, childLevel) {
|
||||
if (!children || children.length === 0) {
|
||||
const icon = parentItem.querySelector('.bi');
|
||||
if(icon) icon.classList.replace('bi-chevron-right', 'bi-dot');
|
||||
return;
|
||||
}
|
||||
|
||||
const nestedGroup = document.createElement('ul');
|
||||
nestedGroup.className = 'list-group mt-2 mb-2 ms-4 nested-group';
|
||||
|
||||
children.forEach(child => {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'list-group-item list-group-item-dark tree-item';
|
||||
li.dataset.level = childLevel;
|
||||
li.dataset.id = child.id;
|
||||
li.dataset.loaded = 'false';
|
||||
|
||||
let content = '';
|
||||
if (childLevel === 'plmn') {
|
||||
li.dataset.tenantId = child.tenant_id;
|
||||
content = `<i class="bi bi-chevron-right me-2"></i> <strong>${child.name}</strong> <small class="text-muted">(MCC: ${child.mcc}, MNC: ${child.mnc})</small>`;
|
||||
} else if (childLevel === 'hnk') {
|
||||
content = `<i class="bi bi-key-fill me-2"></i> <strong>Key ID: ${child.key_id}</strong>`;
|
||||
}
|
||||
li.innerHTML = content;
|
||||
nestedGroup.appendChild(li);
|
||||
});
|
||||
parentItem.after(nestedGroup);
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
155
templates/pages/users.html
Normal file
155
templates/pages/users.html
Normal file
@@ -0,0 +1,155 @@
|
||||
{% extends "layout.html" %}
|
||||
{% set active_page = 'users' %}
|
||||
{% block title %}Dashboard Users - {{ super() }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 id="page-title" class="mb-0">Dashboard Users</h2>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div id="users-view">
|
||||
<p>Retrieve a list of all users from the selected dashboard.</p>
|
||||
<button class="btn btn-primary" id="listUsersBtn">List Users</button>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 my-3 align-items-end">
|
||||
<div class="col-md-6">
|
||||
<label for="tenant-filter" class="form-label">Filter by Tenant</label>
|
||||
<select id="tenant-filter" class="form-select"></select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="search-input" class="form-label">Search Results</label>
|
||||
<input type="text" id="search-input" class="form-control" placeholder="Search any column...">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<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>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script defer>
|
||||
let fullUsersData = [];
|
||||
let currentSort = { column: 'fullname', direction: 'asc' };
|
||||
|
||||
const searchInput = document.getElementById('search-input');
|
||||
const tenantFilter = document.getElementById('tenant-filter');
|
||||
|
||||
function getRoleClass(role) {
|
||||
switch (role) {
|
||||
case 'OWNER': return 'bg-primary';
|
||||
case 'ADMIN': return 'bg-danger';
|
||||
case 'EDITOR': return 'bg-info text-dark';
|
||||
case 'VIEWER': return 'bg-success';
|
||||
default: return 'bg-light text-dark';
|
||||
}
|
||||
}
|
||||
|
||||
function renderTable(data) {
|
||||
let tableHtml = `<table class="table table-dark table-hover table-sm table-compact">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="tree-item" data-sort="fullname">Full Name <i class="bi bi-arrow-down-up"></i></th>
|
||||
<th class="tree-item" data-sort="email">Email <i class="bi bi-arrow-down-up"></i></th>
|
||||
<th class="tree-item" data-sort="role">Role <i class="bi bi-arrow-down-up"></i></th>
|
||||
<th class="tree-item" data-sort="tenant">Tenant <i class="bi bi-arrow-down-up"></i></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>`;
|
||||
|
||||
data.forEach(user => {
|
||||
const roleClass = getRoleClass(user.role);
|
||||
tableHtml += `<tr>
|
||||
<td>${user.fullname || 'N/A'}</td>
|
||||
<td>${user.email || 'N/A'}</td>
|
||||
<td><span class="badge ${roleClass}">${user.role || 'N/A'}</span></td>
|
||||
<td>${user.tenant || 'N/A'}</td>
|
||||
</tr>`;
|
||||
});
|
||||
|
||||
tableHtml += '</tbody></table>';
|
||||
resultsOutput.innerHTML = tableHtml;
|
||||
}
|
||||
|
||||
function populateTenantFilter(data) {
|
||||
const tenants = [...new Set(data.map(item => item.tenant))].sort();
|
||||
tenantFilter.innerHTML = '<option value="">All Tenants</option>';
|
||||
tenants.forEach(tenant => {
|
||||
const option = document.createElement('option');
|
||||
option.value = tenant;
|
||||
option.textContent = tenant;
|
||||
tenantFilter.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
function applyFiltersAndSort() {
|
||||
let filteredData = [...fullUsersData];
|
||||
const searchTerm = searchInput.value.toLowerCase();
|
||||
const selectedTenant = tenantFilter.value;
|
||||
|
||||
if (selectedTenant) {
|
||||
filteredData = filteredData.filter(item => item.tenant === selectedTenant);
|
||||
}
|
||||
|
||||
if (searchTerm) {
|
||||
filteredData = filteredData.filter(item =>
|
||||
Object.values(item).some(value =>
|
||||
String(value).toLowerCase().includes(searchTerm)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
filteredData.sort((a, b) => {
|
||||
const valA = a[currentSort.column] || '';
|
||||
const valB = b[currentSort.column] || '';
|
||||
if (valA < valB) return currentSort.direction === 'asc' ? -1 : 1;
|
||||
if (valA > valB) return currentSort.direction === 'asc' ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
renderTable(filteredData);
|
||||
}
|
||||
|
||||
document.getElementById('listUsersBtn').addEventListener('click', async () => {
|
||||
const dashboard = document.getElementById('dashboard-select').value;
|
||||
const users = await apiCall('/api/users/list', { dashboard });
|
||||
if (users) {
|
||||
fullUsersData = users;
|
||||
populateTenantFilter(fullUsersData);
|
||||
applyFiltersAndSort();
|
||||
}
|
||||
});
|
||||
|
||||
searchInput.addEventListener('input', applyFiltersAndSort);
|
||||
tenantFilter.addEventListener('change', applyFiltersAndSort);
|
||||
|
||||
resultsOutput.addEventListener('click', (event) => {
|
||||
const header = event.target.closest('th[data-sort]');
|
||||
if (header) {
|
||||
const sortColumn = header.dataset.sort;
|
||||
if (currentSort.column === sortColumn) {
|
||||
currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
currentSort.column = sortColumn;
|
||||
currentSort.direction = 'asc';
|
||||
}
|
||||
applyFiltersAndSort();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
80
templates/pages/vpn_status.html
Normal file
80
templates/pages/vpn_status.html
Normal file
@@ -0,0 +1,80 @@
|
||||
{% extends "layout.html" %}
|
||||
{% set active_page = 'vpn_status' %}
|
||||
{% block title %}m2000 Status - {{ super() }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 id="page-title" class="mb-0">m2000 Status</h2>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div id="vpn-view">
|
||||
<p>List the status of all network links from the selected dashboard. From the results table, you can restart the OpenVPN service on a specific device.</p>
|
||||
<p>Note that after a VPN connection is restarted it will take some time for the connection to re-establish. Click List m2000 status to refresh the list.</p>
|
||||
<button class="btn btn-primary" id="listVpnsBtn">List m2000 Status</button>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<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>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script defer>
|
||||
document.getElementById('listVpnsBtn').addEventListener('click', async () => {
|
||||
const dashboard = document.getElementById('dashboard-select').value;
|
||||
const devices = await apiCall('/api/m2000/list', { dashboard });
|
||||
if (devices) {
|
||||
let tableHtml = `<table class="table table-dark table-hover table-sm align-middle table-compact">
|
||||
<thead><tr><th>Serial</th><th>Name</th><th>Subnet</th><th>Status</th><th class="text-center">Action</th></tr></thead>
|
||||
<tbody>`;
|
||||
devices.forEach(d => {
|
||||
const statusClass = getStatusClass(d.status);
|
||||
let actionHtml = '';
|
||||
if (d.status === 'DEPLOYED') {
|
||||
actionHtml = `<a href="#" class="badge bg-danger text-decoration-none restart-btn" data-serial="${d.serial}" data-subnet="${d.subnet}" data-bs-toggle="tooltip" title="Restart VPN"><i class="bi bi-arrow-clockwise"></i> Restart</a>`;
|
||||
}
|
||||
tableHtml += `<tr><td>${d.serial}</td><td>${d.name}</td><td>${d.subnet}</td><td><span class="badge ${statusClass}">${d.status}</span></td><td><div class="d-flex justify-content-center">${actionHtml}</div></td></tr>`;
|
||||
});
|
||||
tableHtml += '</tbody></table>';
|
||||
resultsOutput.innerHTML = tableHtml;
|
||||
initializeTooltips();
|
||||
}
|
||||
});
|
||||
|
||||
resultsOutput.addEventListener('click', async (event) => {
|
||||
const actionLink = event.target.closest('.restart-btn');
|
||||
if (actionLink) {
|
||||
event.preventDefault();
|
||||
const serial = actionLink.dataset.serial;
|
||||
const subnet = actionLink.dataset.subnet;
|
||||
const dashboard = document.getElementById('dashboard-select').value;
|
||||
|
||||
actionLink.disabled = true;
|
||||
actionLink.innerHTML = `<span class="spinner-border spinner-border-sm"></span> Sending...`;
|
||||
|
||||
const result = await apiCall('/api/m2000/restart', { dashboard, serial, subnet }, false);
|
||||
|
||||
if (result) {
|
||||
alert(result.message);
|
||||
}
|
||||
|
||||
actionLink.disabled = false;
|
||||
actionLink.innerHTML = `<i class="bi bi-arrow-clockwise"></i> Restart`;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
131
templates/pages/vpn_switcher.html
Normal file
131
templates/pages/vpn_switcher.html
Normal file
@@ -0,0 +1,131 @@
|
||||
{% extends "layout.html" %}
|
||||
{% set active_page = 'vpn_switcher' %}
|
||||
{% block title %}VPN Switcher - {{ super() }}{% endblock %}
|
||||
|
||||
{% block extra_styles %}
|
||||
<style>
|
||||
.config-output {
|
||||
background-color: #343a40;
|
||||
border-radius: .25rem;
|
||||
padding: 1rem;
|
||||
font-size: 1.1em;
|
||||
white-space: pre-wrap;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 id="page-title" class="mb-0">VPN Switcher</h2>
|
||||
</div>
|
||||
<hr>
|
||||
<div id="switcher-view">
|
||||
<p>Enter a host IP to view and change its current VPN endpoint and system details.</p>
|
||||
<div class="row align-items-end g-3">
|
||||
<div class="col-md-6">
|
||||
<label for="host-ip-input" class="form-label">Host IP Address</label>
|
||||
<input type="text" class="form-control" id="host-ip-input" placeholder="IPv4 Addresses only">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button class="btn btn-primary" id="getInfoBtn">Get Info</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<h4>Result</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>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script defer>
|
||||
const hostIpInput = document.getElementById('host-ip-input');
|
||||
const getInfoBtn = document.getElementById('getInfoBtn');
|
||||
|
||||
async function getInfo() {
|
||||
const hostIp = hostIpInput.value;
|
||||
if (!hostIp) {
|
||||
alert('Please enter a host IP address.');
|
||||
return;
|
||||
}
|
||||
|
||||
history.pushState({}, '', `/vpn-switcher?ip=${hostIp}`);
|
||||
|
||||
const result = await apiCall('/api/vpn/get-config', { host_ip: hostIp });
|
||||
|
||||
if (result) {
|
||||
const details = result.details;
|
||||
const endpointInfo = result.vpn_endpoint;
|
||||
const isUS = endpointInfo.region === 'US';
|
||||
const isEU = endpointInfo.region === 'EU';
|
||||
|
||||
const detailHtml = `
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<strong>Current VPN Endpoint</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>Connected to: <strong>${endpointInfo.region} VPN</strong> (${endpointInfo.ip})</p>
|
||||
<pre class="config-output"><code>${result.vpn_config}</code></pre>
|
||||
<hr>
|
||||
<p>Select a new endpoint:</p>
|
||||
<button class="btn btn-primary me-2 switch-btn" data-region="US" ${isUS ? 'disabled' : ''}>Switch to US-VPN</button>
|
||||
<button class="btn btn-primary switch-btn" data-region="EU" ${isEU ? 'disabled' : ''}>Switch to EU-VPN</button>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="row g-4">
|
||||
<div class="col-md-6">
|
||||
<h5>System Information</h5>
|
||||
<dl class="row">
|
||||
<dt class="col-sm-5">Hostname</dt><dd class="col-sm-7">${details.system.hostname}</dd>
|
||||
<dt class="col-sm-5">Product</dt><dd class="col-sm-7">${details.system.product_name}</dd>
|
||||
<dt class="col-sm-5">Version</dt><dd class="col-sm-7">${details.system.version}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h5>Site Information</h5>
|
||||
<dl class="row">
|
||||
<dt class="col-sm-4">Node Name</dt><dd class="col-sm-8">${details.site.current_node.name}</dd>
|
||||
<dt class="col-sm-4">API Address</dt><dd class="col-sm-8">${details.site.current_node.api_address}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
resultsOutput.innerHTML = detailHtml;
|
||||
}
|
||||
}
|
||||
|
||||
getInfoBtn.addEventListener('click', getInfo);
|
||||
|
||||
resultsOutput.addEventListener('click', async (event) => {
|
||||
const switchButton = event.target.closest('.switch-btn');
|
||||
if (switchButton) {
|
||||
const hostIp = hostIpInput.value;
|
||||
const region = switchButton.dataset.region;
|
||||
|
||||
switchButton.disabled = true;
|
||||
switchButton.innerHTML = `<span class="spinner-border spinner-border-sm"></span> Switching...`;
|
||||
|
||||
const result = await apiCall('/api/vpn/set-endpoint', { host_ip: hostIp, region: region }, false);
|
||||
|
||||
if (result) {
|
||||
alert(result.message);
|
||||
getInfo(); // Refresh the info after a successful switch
|
||||
} else {
|
||||
getInfo(); // Also refresh on failure to restore the button state
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const ipFromUrl = '{{ ip_from_url|default("", True) }}';
|
||||
if (ipFromUrl) {
|
||||
hostIpInput.value = ipFromUrl;
|
||||
getInfoBtn.click();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
247
venv/bin/Activate.ps1
Normal file
247
venv/bin/Activate.ps1
Normal file
@@ -0,0 +1,247 @@
|
||||
<#
|
||||
.Synopsis
|
||||
Activate a Python virtual environment for the current PowerShell session.
|
||||
|
||||
.Description
|
||||
Pushes the python executable for a virtual environment to the front of the
|
||||
$Env:PATH environment variable and sets the prompt to signify that you are
|
||||
in a Python virtual environment. Makes use of the command line switches as
|
||||
well as the `pyvenv.cfg` file values present in the virtual environment.
|
||||
|
||||
.Parameter VenvDir
|
||||
Path to the directory that contains the virtual environment to activate. The
|
||||
default value for this is the parent of the directory that the Activate.ps1
|
||||
script is located within.
|
||||
|
||||
.Parameter Prompt
|
||||
The prompt prefix to display when this virtual environment is activated. By
|
||||
default, this prompt is the name of the virtual environment folder (VenvDir)
|
||||
surrounded by parentheses and followed by a single space (ie. '(.venv) ').
|
||||
|
||||
.Example
|
||||
Activate.ps1
|
||||
Activates the Python virtual environment that contains the Activate.ps1 script.
|
||||
|
||||
.Example
|
||||
Activate.ps1 -Verbose
|
||||
Activates the Python virtual environment that contains the Activate.ps1 script,
|
||||
and shows extra information about the activation as it executes.
|
||||
|
||||
.Example
|
||||
Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv
|
||||
Activates the Python virtual environment located in the specified location.
|
||||
|
||||
.Example
|
||||
Activate.ps1 -Prompt "MyPython"
|
||||
Activates the Python virtual environment that contains the Activate.ps1 script,
|
||||
and prefixes the current prompt with the specified string (surrounded in
|
||||
parentheses) while the virtual environment is active.
|
||||
|
||||
.Notes
|
||||
On Windows, it may be required to enable this Activate.ps1 script by setting the
|
||||
execution policy for the user. You can do this by issuing the following PowerShell
|
||||
command:
|
||||
|
||||
PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
|
||||
|
||||
For more information on Execution Policies:
|
||||
https://go.microsoft.com/fwlink/?LinkID=135170
|
||||
|
||||
#>
|
||||
Param(
|
||||
[Parameter(Mandatory = $false)]
|
||||
[String]
|
||||
$VenvDir,
|
||||
[Parameter(Mandatory = $false)]
|
||||
[String]
|
||||
$Prompt
|
||||
)
|
||||
|
||||
<# Function declarations --------------------------------------------------- #>
|
||||
|
||||
<#
|
||||
.Synopsis
|
||||
Remove all shell session elements added by the Activate script, including the
|
||||
addition of the virtual environment's Python executable from the beginning of
|
||||
the PATH variable.
|
||||
|
||||
.Parameter NonDestructive
|
||||
If present, do not remove this function from the global namespace for the
|
||||
session.
|
||||
|
||||
#>
|
||||
function global:deactivate ([switch]$NonDestructive) {
|
||||
# Revert to original values
|
||||
|
||||
# The prior prompt:
|
||||
if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) {
|
||||
Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt
|
||||
Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT
|
||||
}
|
||||
|
||||
# The prior PYTHONHOME:
|
||||
if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) {
|
||||
Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME
|
||||
Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME
|
||||
}
|
||||
|
||||
# The prior PATH:
|
||||
if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) {
|
||||
Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH
|
||||
Remove-Item -Path Env:_OLD_VIRTUAL_PATH
|
||||
}
|
||||
|
||||
# Just remove the VIRTUAL_ENV altogether:
|
||||
if (Test-Path -Path Env:VIRTUAL_ENV) {
|
||||
Remove-Item -Path env:VIRTUAL_ENV
|
||||
}
|
||||
|
||||
# Just remove VIRTUAL_ENV_PROMPT altogether.
|
||||
if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) {
|
||||
Remove-Item -Path env:VIRTUAL_ENV_PROMPT
|
||||
}
|
||||
|
||||
# Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether:
|
||||
if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) {
|
||||
Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force
|
||||
}
|
||||
|
||||
# Leave deactivate function in the global namespace if requested:
|
||||
if (-not $NonDestructive) {
|
||||
Remove-Item -Path function:deactivate
|
||||
}
|
||||
}
|
||||
|
||||
<#
|
||||
.Description
|
||||
Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the
|
||||
given folder, and returns them in a map.
|
||||
|
||||
For each line in the pyvenv.cfg file, if that line can be parsed into exactly
|
||||
two strings separated by `=` (with any amount of whitespace surrounding the =)
|
||||
then it is considered a `key = value` line. The left hand string is the key,
|
||||
the right hand is the value.
|
||||
|
||||
If the value starts with a `'` or a `"` then the first and last character is
|
||||
stripped from the value before being captured.
|
||||
|
||||
.Parameter ConfigDir
|
||||
Path to the directory that contains the `pyvenv.cfg` file.
|
||||
#>
|
||||
function Get-PyVenvConfig(
|
||||
[String]
|
||||
$ConfigDir
|
||||
) {
|
||||
Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg"
|
||||
|
||||
# Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue).
|
||||
$pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue
|
||||
|
||||
# An empty map will be returned if no config file is found.
|
||||
$pyvenvConfig = @{ }
|
||||
|
||||
if ($pyvenvConfigPath) {
|
||||
|
||||
Write-Verbose "File exists, parse `key = value` lines"
|
||||
$pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath
|
||||
|
||||
$pyvenvConfigContent | ForEach-Object {
|
||||
$keyval = $PSItem -split "\s*=\s*", 2
|
||||
if ($keyval[0] -and $keyval[1]) {
|
||||
$val = $keyval[1]
|
||||
|
||||
# Remove extraneous quotations around a string value.
|
||||
if ("'""".Contains($val.Substring(0, 1))) {
|
||||
$val = $val.Substring(1, $val.Length - 2)
|
||||
}
|
||||
|
||||
$pyvenvConfig[$keyval[0]] = $val
|
||||
Write-Verbose "Adding Key: '$($keyval[0])'='$val'"
|
||||
}
|
||||
}
|
||||
}
|
||||
return $pyvenvConfig
|
||||
}
|
||||
|
||||
|
||||
<# Begin Activate script --------------------------------------------------- #>
|
||||
|
||||
# Determine the containing directory of this script
|
||||
$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||
$VenvExecDir = Get-Item -Path $VenvExecPath
|
||||
|
||||
Write-Verbose "Activation script is located in path: '$VenvExecPath'"
|
||||
Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)"
|
||||
Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)"
|
||||
|
||||
# Set values required in priority: CmdLine, ConfigFile, Default
|
||||
# First, get the location of the virtual environment, it might not be
|
||||
# VenvExecDir if specified on the command line.
|
||||
if ($VenvDir) {
|
||||
Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values"
|
||||
}
|
||||
else {
|
||||
Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir."
|
||||
$VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/")
|
||||
Write-Verbose "VenvDir=$VenvDir"
|
||||
}
|
||||
|
||||
# Next, read the `pyvenv.cfg` file to determine any required value such
|
||||
# as `prompt`.
|
||||
$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir
|
||||
|
||||
# Next, set the prompt from the command line, or the config file, or
|
||||
# just use the name of the virtual environment folder.
|
||||
if ($Prompt) {
|
||||
Write-Verbose "Prompt specified as argument, using '$Prompt'"
|
||||
}
|
||||
else {
|
||||
Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value"
|
||||
if ($pyvenvCfg -and $pyvenvCfg['prompt']) {
|
||||
Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'"
|
||||
$Prompt = $pyvenvCfg['prompt'];
|
||||
}
|
||||
else {
|
||||
Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)"
|
||||
Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'"
|
||||
$Prompt = Split-Path -Path $venvDir -Leaf
|
||||
}
|
||||
}
|
||||
|
||||
Write-Verbose "Prompt = '$Prompt'"
|
||||
Write-Verbose "VenvDir='$VenvDir'"
|
||||
|
||||
# Deactivate any currently active virtual environment, but leave the
|
||||
# deactivate function in place.
|
||||
deactivate -nondestructive
|
||||
|
||||
# Now set the environment variable VIRTUAL_ENV, used by many tools to determine
|
||||
# that there is an activated venv.
|
||||
$env:VIRTUAL_ENV = $VenvDir
|
||||
|
||||
if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) {
|
||||
|
||||
Write-Verbose "Setting prompt to '$Prompt'"
|
||||
|
||||
# Set the prompt to include the env name
|
||||
# Make sure _OLD_VIRTUAL_PROMPT is global
|
||||
function global:_OLD_VIRTUAL_PROMPT { "" }
|
||||
Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT
|
||||
New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt
|
||||
|
||||
function global:prompt {
|
||||
Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) "
|
||||
_OLD_VIRTUAL_PROMPT
|
||||
}
|
||||
$env:VIRTUAL_ENV_PROMPT = $Prompt
|
||||
}
|
||||
|
||||
# Clear PYTHONHOME
|
||||
if (Test-Path -Path Env:PYTHONHOME) {
|
||||
Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME
|
||||
Remove-Item -Path Env:PYTHONHOME
|
||||
}
|
||||
|
||||
# Add the venv to the PATH
|
||||
Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH
|
||||
$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH"
|
||||
69
venv/bin/activate
Normal file
69
venv/bin/activate
Normal file
@@ -0,0 +1,69 @@
|
||||
# This file must be used with "source bin/activate" *from bash*
|
||||
# you cannot run it directly
|
||||
|
||||
deactivate () {
|
||||
# reset old environment variables
|
||||
if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
|
||||
PATH="${_OLD_VIRTUAL_PATH:-}"
|
||||
export PATH
|
||||
unset _OLD_VIRTUAL_PATH
|
||||
fi
|
||||
if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
|
||||
PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
|
||||
export PYTHONHOME
|
||||
unset _OLD_VIRTUAL_PYTHONHOME
|
||||
fi
|
||||
|
||||
# This should detect bash and zsh, which have a hash command that must
|
||||
# be called to get it to forget past commands. Without forgetting
|
||||
# past commands the $PATH changes we made may not be respected
|
||||
if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then
|
||||
hash -r 2> /dev/null
|
||||
fi
|
||||
|
||||
if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
|
||||
PS1="${_OLD_VIRTUAL_PS1:-}"
|
||||
export PS1
|
||||
unset _OLD_VIRTUAL_PS1
|
||||
fi
|
||||
|
||||
unset VIRTUAL_ENV
|
||||
unset VIRTUAL_ENV_PROMPT
|
||||
if [ ! "${1:-}" = "nondestructive" ] ; then
|
||||
# Self destruct!
|
||||
unset -f deactivate
|
||||
fi
|
||||
}
|
||||
|
||||
# unset irrelevant variables
|
||||
deactivate nondestructive
|
||||
|
||||
VIRTUAL_ENV=/home/mjensen/network_tool/venv
|
||||
export VIRTUAL_ENV
|
||||
|
||||
_OLD_VIRTUAL_PATH="$PATH"
|
||||
PATH="$VIRTUAL_ENV/"bin":$PATH"
|
||||
export PATH
|
||||
|
||||
# unset PYTHONHOME if set
|
||||
# this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
|
||||
# could use `if (set -u; : $PYTHONHOME) ;` in bash
|
||||
if [ -n "${PYTHONHOME:-}" ] ; then
|
||||
_OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
|
||||
unset PYTHONHOME
|
||||
fi
|
||||
|
||||
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
|
||||
_OLD_VIRTUAL_PS1="${PS1:-}"
|
||||
PS1='(venv) '"${PS1:-}"
|
||||
export PS1
|
||||
VIRTUAL_ENV_PROMPT='(venv) '
|
||||
export VIRTUAL_ENV_PROMPT
|
||||
fi
|
||||
|
||||
# This should detect bash and zsh, which have a hash command that must
|
||||
# be called to get it to forget past commands. Without forgetting
|
||||
# past commands the $PATH changes we made may not be respected
|
||||
if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then
|
||||
hash -r 2> /dev/null
|
||||
fi
|
||||
26
venv/bin/activate.csh
Normal file
26
venv/bin/activate.csh
Normal file
@@ -0,0 +1,26 @@
|
||||
# This file must be used with "source bin/activate.csh" *from csh*.
|
||||
# You cannot run it directly.
|
||||
# Created by Davide Di Blasi <davidedb@gmail.com>.
|
||||
# Ported to Python 3.3 venv by Andrew Svetlov <andrew.svetlov@gmail.com>
|
||||
|
||||
alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate'
|
||||
|
||||
# Unset irrelevant variables.
|
||||
deactivate nondestructive
|
||||
|
||||
setenv VIRTUAL_ENV /home/mjensen/network_tool/venv
|
||||
|
||||
set _OLD_VIRTUAL_PATH="$PATH"
|
||||
setenv PATH "$VIRTUAL_ENV/"bin":$PATH"
|
||||
|
||||
|
||||
set _OLD_VIRTUAL_PROMPT="$prompt"
|
||||
|
||||
if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
|
||||
set prompt = '(venv) '"$prompt"
|
||||
setenv VIRTUAL_ENV_PROMPT '(venv) '
|
||||
endif
|
||||
|
||||
alias pydoc python -m pydoc
|
||||
|
||||
rehash
|
||||
69
venv/bin/activate.fish
Normal file
69
venv/bin/activate.fish
Normal file
@@ -0,0 +1,69 @@
|
||||
# This file must be used with "source <venv>/bin/activate.fish" *from fish*
|
||||
# (https://fishshell.com/); you cannot run it directly.
|
||||
|
||||
function deactivate -d "Exit virtual environment and return to normal shell environment"
|
||||
# reset old environment variables
|
||||
if test -n "$_OLD_VIRTUAL_PATH"
|
||||
set -gx PATH $_OLD_VIRTUAL_PATH
|
||||
set -e _OLD_VIRTUAL_PATH
|
||||
end
|
||||
if test -n "$_OLD_VIRTUAL_PYTHONHOME"
|
||||
set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME
|
||||
set -e _OLD_VIRTUAL_PYTHONHOME
|
||||
end
|
||||
|
||||
if test -n "$_OLD_FISH_PROMPT_OVERRIDE"
|
||||
set -e _OLD_FISH_PROMPT_OVERRIDE
|
||||
# prevents error when using nested fish instances (Issue #93858)
|
||||
if functions -q _old_fish_prompt
|
||||
functions -e fish_prompt
|
||||
functions -c _old_fish_prompt fish_prompt
|
||||
functions -e _old_fish_prompt
|
||||
end
|
||||
end
|
||||
|
||||
set -e VIRTUAL_ENV
|
||||
set -e VIRTUAL_ENV_PROMPT
|
||||
if test "$argv[1]" != "nondestructive"
|
||||
# Self-destruct!
|
||||
functions -e deactivate
|
||||
end
|
||||
end
|
||||
|
||||
# Unset irrelevant variables.
|
||||
deactivate nondestructive
|
||||
|
||||
set -gx VIRTUAL_ENV /home/mjensen/network_tool/venv
|
||||
|
||||
set -gx _OLD_VIRTUAL_PATH $PATH
|
||||
set -gx PATH "$VIRTUAL_ENV/"bin $PATH
|
||||
|
||||
# Unset PYTHONHOME if set.
|
||||
if set -q PYTHONHOME
|
||||
set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME
|
||||
set -e PYTHONHOME
|
||||
end
|
||||
|
||||
if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
|
||||
# fish uses a function instead of an env var to generate the prompt.
|
||||
|
||||
# Save the current fish_prompt function as the function _old_fish_prompt.
|
||||
functions -c fish_prompt _old_fish_prompt
|
||||
|
||||
# With the original prompt function renamed, we can override with our own.
|
||||
function fish_prompt
|
||||
# Save the return status of the last command.
|
||||
set -l old_status $status
|
||||
|
||||
# Output the venv prompt; color taken from the blue of the Python logo.
|
||||
printf "%s%s%s" (set_color 4B8BBE) '(venv) ' (set_color normal)
|
||||
|
||||
# Restore the return status of the previous command.
|
||||
echo "exit $old_status" | .
|
||||
# Output the original/"old" prompt.
|
||||
_old_fish_prompt
|
||||
end
|
||||
|
||||
set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV"
|
||||
set -gx VIRTUAL_ENV_PROMPT '(venv) '
|
||||
end
|
||||
8
venv/bin/flask
Executable file
8
venv/bin/flask
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/home/mjensen/network_tool/venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from flask.cli import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
||||
8
venv/bin/gunicorn
Executable file
8
venv/bin/gunicorn
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/home/mjensen/network_tool/venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from gunicorn.app.wsgiapp import run
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(run())
|
||||
8
venv/bin/normalizer
Executable file
8
venv/bin/normalizer
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/home/mjensen/network_tool/venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from charset_normalizer import cli
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(cli.cli_detect())
|
||||
8
venv/bin/pip
Executable file
8
venv/bin/pip
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/home/mjensen/network_tool/venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from pip._internal.cli.main import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
||||
8
venv/bin/pip3
Executable file
8
venv/bin/pip3
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/home/mjensen/network_tool/venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from pip._internal.cli.main import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
||||
8
venv/bin/pip3.10
Executable file
8
venv/bin/pip3.10
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/home/mjensen/network_tool/venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from pip._internal.cli.main import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
||||
1
venv/bin/python
Symbolic link
1
venv/bin/python
Symbolic link
@@ -0,0 +1 @@
|
||||
python3
|
||||
1
venv/bin/python3
Symbolic link
1
venv/bin/python3
Symbolic link
@@ -0,0 +1 @@
|
||||
/usr/bin/python3
|
||||
1
venv/bin/python3.10
Symbolic link
1
venv/bin/python3.10
Symbolic link
@@ -0,0 +1 @@
|
||||
python3
|
||||
@@ -0,0 +1 @@
|
||||
pip
|
||||
@@ -0,0 +1,28 @@
|
||||
Copyright 2010 Pallets
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
||||
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -0,0 +1,92 @@
|
||||
Metadata-Version: 2.1
|
||||
Name: MarkupSafe
|
||||
Version: 3.0.2
|
||||
Summary: Safely add untrusted strings to HTML/XML markup.
|
||||
Maintainer-email: Pallets <contact@palletsprojects.com>
|
||||
License: Copyright 2010 Pallets
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
||||
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Project-URL: Donate, https://palletsprojects.com/donate
|
||||
Project-URL: Documentation, https://markupsafe.palletsprojects.com/
|
||||
Project-URL: Changes, https://markupsafe.palletsprojects.com/changes/
|
||||
Project-URL: Source, https://github.com/pallets/markupsafe/
|
||||
Project-URL: Chat, https://discord.gg/pallets
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Environment :: Web Environment
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: BSD License
|
||||
Classifier: Operating System :: OS Independent
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
||||
Classifier: Topic :: Text Processing :: Markup :: HTML
|
||||
Classifier: Typing :: Typed
|
||||
Requires-Python: >=3.9
|
||||
Description-Content-Type: text/markdown
|
||||
License-File: LICENSE.txt
|
||||
|
||||
# MarkupSafe
|
||||
|
||||
MarkupSafe implements a text object that escapes characters so it is
|
||||
safe to use in HTML and XML. Characters that have special meanings are
|
||||
replaced so that they display as the actual characters. This mitigates
|
||||
injection attacks, meaning untrusted user input can safely be displayed
|
||||
on a page.
|
||||
|
||||
|
||||
## Examples
|
||||
|
||||
```pycon
|
||||
>>> from markupsafe import Markup, escape
|
||||
|
||||
>>> # escape replaces special characters and wraps in Markup
|
||||
>>> escape("<script>alert(document.cookie);</script>")
|
||||
Markup('<script>alert(document.cookie);</script>')
|
||||
|
||||
>>> # wrap in Markup to mark text "safe" and prevent escaping
|
||||
>>> Markup("<strong>Hello</strong>")
|
||||
Markup('<strong>hello</strong>')
|
||||
|
||||
>>> escape(Markup("<strong>Hello</strong>"))
|
||||
Markup('<strong>hello</strong>')
|
||||
|
||||
>>> # Markup is a str subclass
|
||||
>>> # methods and operators escape their arguments
|
||||
>>> template = Markup("Hello <em>{name}</em>")
|
||||
>>> template.format(name='"World"')
|
||||
Markup('Hello <em>"World"</em>')
|
||||
```
|
||||
|
||||
## Donate
|
||||
|
||||
The Pallets organization develops and supports MarkupSafe and other
|
||||
popular packages. In order to grow the community of contributors and
|
||||
users, and allow the maintainers to devote more time to the projects,
|
||||
[please donate today][].
|
||||
|
||||
[please donate today]: https://palletsprojects.com/donate
|
||||
@@ -0,0 +1,14 @@
|
||||
MarkupSafe-3.0.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
MarkupSafe-3.0.2.dist-info/LICENSE.txt,sha256=SJqOEQhQntmKN7uYPhHg9-HTHwvY-Zp5yESOf_N9B-o,1475
|
||||
MarkupSafe-3.0.2.dist-info/METADATA,sha256=aAwbZhSmXdfFuMM-rEHpeiHRkBOGESyVLJIuwzHP-nw,3975
|
||||
MarkupSafe-3.0.2.dist-info/RECORD,,
|
||||
MarkupSafe-3.0.2.dist-info/WHEEL,sha256=_kVlewavvOSnwZE_whBk3jlE_Ob-nL5GvlVcLkpXSD8,151
|
||||
MarkupSafe-3.0.2.dist-info/top_level.txt,sha256=qy0Plje5IJuvsCBjejJyhDCjEAdcDLK_2agVcex8Z6U,11
|
||||
markupsafe/__init__.py,sha256=sr-U6_27DfaSrj5jnHYxWN-pvhM27sjlDplMDPZKm7k,13214
|
||||
markupsafe/__pycache__/__init__.cpython-310.pyc,,
|
||||
markupsafe/__pycache__/_native.cpython-310.pyc,,
|
||||
markupsafe/_native.py,sha256=hSLs8Jmz5aqayuengJJ3kdT5PwNpBWpKrmQSdipndC8,210
|
||||
markupsafe/_speedups.c,sha256=O7XulmTo-epI6n2FtMVOrJXl8EAaIwD2iNYmBI5SEoQ,4149
|
||||
markupsafe/_speedups.cpython-310-x86_64-linux-gnu.so,sha256=x4RoxWgyqAEokk-AZrWvrLDxLE-dm-zZSZYV_gOiLJA,34976
|
||||
markupsafe/_speedups.pyi,sha256=ENd1bYe7gbBUf2ywyYWOGUpnXOHNJ-cgTNqetlW8h5k,41
|
||||
markupsafe/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
@@ -0,0 +1,6 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: setuptools (75.2.0)
|
||||
Root-Is-Purelib: false
|
||||
Tag: cp310-cp310-manylinux_2_17_x86_64
|
||||
Tag: cp310-cp310-manylinux2014_x86_64
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
markupsafe
|
||||
@@ -0,0 +1 @@
|
||||
pip
|
||||
174
venv/lib/python3.10/site-packages/PyNaCl-1.5.0.dist-info/LICENSE
Normal file
174
venv/lib/python3.10/site-packages/PyNaCl-1.5.0.dist-info/LICENSE
Normal file
@@ -0,0 +1,174 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
@@ -0,0 +1,245 @@
|
||||
Metadata-Version: 2.1
|
||||
Name: PyNaCl
|
||||
Version: 1.5.0
|
||||
Summary: Python binding to the Networking and Cryptography (NaCl) library
|
||||
Home-page: https://github.com/pyca/pynacl/
|
||||
Author: The PyNaCl developers
|
||||
Author-email: cryptography-dev@python.org
|
||||
License: Apache License 2.0
|
||||
Platform: UNKNOWN
|
||||
Classifier: Programming Language :: Python :: Implementation :: CPython
|
||||
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python :: 3.6
|
||||
Classifier: Programming Language :: Python :: 3.7
|
||||
Classifier: Programming Language :: Python :: 3.8
|
||||
Classifier: Programming Language :: Python :: 3.9
|
||||
Classifier: Programming Language :: Python :: 3.10
|
||||
Requires-Python: >=3.6
|
||||
Requires-Dist: cffi (>=1.4.1)
|
||||
Provides-Extra: docs
|
||||
Requires-Dist: sphinx (>=1.6.5) ; extra == 'docs'
|
||||
Requires-Dist: sphinx-rtd-theme ; extra == 'docs'
|
||||
Provides-Extra: tests
|
||||
Requires-Dist: pytest (!=3.3.0,>=3.2.1) ; extra == 'tests'
|
||||
Requires-Dist: hypothesis (>=3.27.0) ; extra == 'tests'
|
||||
|
||||
===============================================
|
||||
PyNaCl: Python binding to the libsodium library
|
||||
===============================================
|
||||
|
||||
.. image:: https://img.shields.io/pypi/v/pynacl.svg
|
||||
:target: https://pypi.org/project/PyNaCl/
|
||||
:alt: Latest Version
|
||||
|
||||
.. image:: https://codecov.io/github/pyca/pynacl/coverage.svg?branch=main
|
||||
:target: https://codecov.io/github/pyca/pynacl?branch=main
|
||||
|
||||
.. image:: https://img.shields.io/pypi/pyversions/pynacl.svg
|
||||
:target: https://pypi.org/project/PyNaCl/
|
||||
:alt: Compatible Python Versions
|
||||
|
||||
PyNaCl is a Python binding to `libsodium`_, which is a fork of the
|
||||
`Networking and Cryptography library`_. These libraries have a stated goal of
|
||||
improving usability, security and speed. It supports Python 3.6+ as well as
|
||||
PyPy 3.
|
||||
|
||||
.. _libsodium: https://github.com/jedisct1/libsodium
|
||||
.. _Networking and Cryptography library: https://nacl.cr.yp.to/
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
* Digital signatures
|
||||
* Secret-key encryption
|
||||
* Public-key encryption
|
||||
* Hashing and message authentication
|
||||
* Password based key derivation and password hashing
|
||||
|
||||
`Changelog`_
|
||||
------------
|
||||
|
||||
.. _Changelog: https://pynacl.readthedocs.io/en/stable/changelog/
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
Binary wheel install
|
||||
--------------------
|
||||
|
||||
PyNaCl ships as a binary wheel on macOS, Windows and Linux ``manylinux1`` [#many]_ ,
|
||||
so all dependencies are included. Make sure you have an up-to-date pip
|
||||
and run:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ pip install pynacl
|
||||
|
||||
Faster wheel build
|
||||
------------------
|
||||
|
||||
You can define the environment variable ``LIBSODIUM_MAKE_ARGS`` to pass arguments to ``make``
|
||||
and enable `parallelization`_:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ LIBSODIUM_MAKE_ARGS=-j4 pip install pynacl
|
||||
|
||||
Linux source build
|
||||
------------------
|
||||
|
||||
PyNaCl relies on `libsodium`_, a portable C library. A copy is bundled
|
||||
with PyNaCl so to install you can run:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ pip install pynacl
|
||||
|
||||
If you'd prefer to use the version of ``libsodium`` provided by your
|
||||
distribution, you can disable the bundled copy during install by running:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ SODIUM_INSTALL=system pip install pynacl
|
||||
|
||||
.. warning:: Usage of the legacy ``easy_install`` command provided by setuptools
|
||||
is generally discouraged, and is completely unsupported in PyNaCl's case.
|
||||
|
||||
.. _parallelization: https://www.gnu.org/software/make/manual/html_node/Parallel.html
|
||||
|
||||
.. _libsodium: https://github.com/jedisct1/libsodium
|
||||
|
||||
.. [#many] `manylinux1 wheels <https://www.python.org/dev/peps/pep-0513/>`_
|
||||
are built on a baseline linux environment based on Centos 5.11
|
||||
and should work on most x86 and x86_64 glibc based linux environments.
|
||||
|
||||
Changelog
|
||||
=========
|
||||
|
||||
1.5.0 (2022-01-07)
|
||||
------------------
|
||||
|
||||
* **BACKWARDS INCOMPATIBLE:** Removed support for Python 2.7 and Python 3.5.
|
||||
* **BACKWARDS INCOMPATIBLE:** We no longer distribute ``manylinux1``
|
||||
wheels.
|
||||
* Added ``manylinux2014``, ``manylinux_2_24``, ``musllinux``, and macOS
|
||||
``universal2`` wheels (the latter supports macOS ``arm64``).
|
||||
* Update ``libsodium`` to 1.0.18-stable (July 25, 2021 release).
|
||||
* Add inline type hints.
|
||||
|
||||
1.4.0 (2020-05-25)
|
||||
------------------
|
||||
|
||||
* Update ``libsodium`` to 1.0.18.
|
||||
* **BACKWARDS INCOMPATIBLE:** We no longer distribute 32-bit ``manylinux1``
|
||||
wheels. Continuing to produce them was a maintenance burden.
|
||||
* Added support for Python 3.8, and removed support for Python 3.4.
|
||||
* Add low level bindings for extracting the seed and the public key
|
||||
from crypto_sign_ed25519 secret key
|
||||
* Add low level bindings for deterministic random generation.
|
||||
* Add ``wheel`` and ``setuptools`` setup_requirements in ``setup.py`` (#485)
|
||||
* Fix checks on very slow builders (#481, #495)
|
||||
* Add low-level bindings to ed25519 arithmetic functions
|
||||
* Update low-level blake2b state implementation
|
||||
* Fix wrong short-input behavior of SealedBox.decrypt() (#517)
|
||||
* Raise CryptPrefixError exception instead of InvalidkeyError when trying
|
||||
to check a password against a verifier stored in a unknown format (#519)
|
||||
* Add support for minimal builds of libsodium. Trying to call functions
|
||||
not available in a minimal build will raise an UnavailableError
|
||||
exception. To compile a minimal build of the bundled libsodium, set
|
||||
the SODIUM_INSTALL_MINIMAL environment variable to any non-empty
|
||||
string (e.g. ``SODIUM_INSTALL_MINIMAL=1``) for setup.
|
||||
|
||||
1.3.0 2018-09-26
|
||||
----------------
|
||||
|
||||
* Added support for Python 3.7.
|
||||
* Update ``libsodium`` to 1.0.16.
|
||||
* Run and test all code examples in PyNaCl docs through sphinx's
|
||||
doctest builder.
|
||||
* Add low-level bindings for chacha20-poly1305 AEAD constructions.
|
||||
* Add low-level bindings for the chacha20-poly1305 secretstream constructions.
|
||||
* Add low-level bindings for ed25519ph pre-hashed signing construction.
|
||||
* Add low-level bindings for constant-time increment and addition
|
||||
on fixed-precision big integers represented as little-endian
|
||||
byte sequences.
|
||||
* Add low-level bindings for the ISO/IEC 7816-4 compatible padding API.
|
||||
* Add low-level bindings for libsodium's crypto_kx... key exchange
|
||||
construction.
|
||||
* Set hypothesis deadline to None in tests/test_pwhash.py to avoid
|
||||
incorrect test failures on slower processor architectures. GitHub
|
||||
issue #370
|
||||
|
||||
1.2.1 - 2017-12-04
|
||||
------------------
|
||||
|
||||
* Update hypothesis minimum allowed version.
|
||||
* Infrastructure: add proper configuration for readthedocs builder
|
||||
runtime environment.
|
||||
|
||||
1.2.0 - 2017-11-01
|
||||
------------------
|
||||
|
||||
* Update ``libsodium`` to 1.0.15.
|
||||
* Infrastructure: add jenkins support for automatic build of
|
||||
``manylinux1`` binary wheels
|
||||
* Added support for ``SealedBox`` construction.
|
||||
* Added support for ``argon2i`` and ``argon2id`` password hashing constructs
|
||||
and restructured high-level password hashing implementation to expose
|
||||
the same interface for all hashers.
|
||||
* Added support for 128 bit ``siphashx24`` variant of ``siphash24``.
|
||||
* Added support for ``from_seed`` APIs for X25519 keypair generation.
|
||||
* Dropped support for Python 3.3.
|
||||
|
||||
1.1.2 - 2017-03-31
|
||||
------------------
|
||||
|
||||
* reorder link time library search path when using bundled
|
||||
libsodium
|
||||
|
||||
1.1.1 - 2017-03-15
|
||||
------------------
|
||||
|
||||
* Fixed a circular import bug in ``nacl.utils``.
|
||||
|
||||
1.1.0 - 2017-03-14
|
||||
------------------
|
||||
|
||||
* Dropped support for Python 2.6.
|
||||
* Added ``shared_key()`` method on ``Box``.
|
||||
* You can now pass ``None`` to ``nonce`` when encrypting with ``Box`` or
|
||||
``SecretBox`` and it will automatically generate a random nonce.
|
||||
* Added support for ``siphash24``.
|
||||
* Added support for ``blake2b``.
|
||||
* Added support for ``scrypt``.
|
||||
* Update ``libsodium`` to 1.0.11.
|
||||
* Default to the bundled ``libsodium`` when compiling.
|
||||
* All raised exceptions are defined mixing-in
|
||||
``nacl.exceptions.CryptoError``
|
||||
|
||||
1.0.1 - 2016-01-24
|
||||
------------------
|
||||
|
||||
* Fix an issue with absolute paths that prevented the creation of wheels.
|
||||
|
||||
1.0 - 2016-01-23
|
||||
----------------
|
||||
|
||||
* PyNaCl has been ported to use the new APIs available in cffi 1.0+.
|
||||
Due to this change we no longer support PyPy releases older than 2.6.
|
||||
* Python 3.2 support has been dropped.
|
||||
* Functions to convert between Ed25519 and Curve25519 keys have been added.
|
||||
|
||||
0.3.0 - 2015-03-04
|
||||
------------------
|
||||
|
||||
* The low-level API (`nacl.c.*`) has been changed to match the
|
||||
upstream NaCl C/C++ conventions (as well as those of other NaCl bindings).
|
||||
The order of arguments and return values has changed significantly. To
|
||||
avoid silent failures, `nacl.c` has been removed, and replaced with
|
||||
`nacl.bindings` (with the new argument ordering). If you have code which
|
||||
calls these functions (e.g. `nacl.c.crypto_box_keypair()`), you must review
|
||||
the new docstrings and update your code/imports to match the new
|
||||
conventions.
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
PyNaCl-1.5.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
PyNaCl-1.5.0.dist-info/LICENSE,sha256=0xdK1j5yHUydzLitQyCEiZLTFDabxGMZcgtYAskVP-k,9694
|
||||
PyNaCl-1.5.0.dist-info/METADATA,sha256=OJaXCiHgNRywLY9cj3X2euddUPZ4dnyyqAQMU01X4j0,8634
|
||||
PyNaCl-1.5.0.dist-info/RECORD,,
|
||||
PyNaCl-1.5.0.dist-info/WHEEL,sha256=TIQeZFe3DwXBO5UGlCH1aKpf5Cx6FJLbIUqd-Sq2juI,185
|
||||
PyNaCl-1.5.0.dist-info/top_level.txt,sha256=wfdEOI_G2RIzmzsMyhpqP17HUh6Jcqi99to9aHLEslo,13
|
||||
nacl/__init__.py,sha256=0IUunzBT8_Jn0DUdHacBExOYeAEMggo8slkfjo7O0XM,1116
|
||||
nacl/__pycache__/__init__.cpython-310.pyc,,
|
||||
nacl/__pycache__/encoding.cpython-310.pyc,,
|
||||
nacl/__pycache__/exceptions.cpython-310.pyc,,
|
||||
nacl/__pycache__/hash.cpython-310.pyc,,
|
||||
nacl/__pycache__/hashlib.cpython-310.pyc,,
|
||||
nacl/__pycache__/public.cpython-310.pyc,,
|
||||
nacl/__pycache__/secret.cpython-310.pyc,,
|
||||
nacl/__pycache__/signing.cpython-310.pyc,,
|
||||
nacl/__pycache__/utils.cpython-310.pyc,,
|
||||
nacl/_sodium.abi3.so,sha256=uJ6RwSnbb9wO4esR0bVUqrfFHtBOGm34IQIdmaE1fGY,2740136
|
||||
nacl/bindings/__init__.py,sha256=BDlStrds2EuUS4swOL4pnf92PWVS_CHRCptX3KhEX-s,16997
|
||||
nacl/bindings/__pycache__/__init__.cpython-310.pyc,,
|
||||
nacl/bindings/__pycache__/crypto_aead.cpython-310.pyc,,
|
||||
nacl/bindings/__pycache__/crypto_box.cpython-310.pyc,,
|
||||
nacl/bindings/__pycache__/crypto_core.cpython-310.pyc,,
|
||||
nacl/bindings/__pycache__/crypto_generichash.cpython-310.pyc,,
|
||||
nacl/bindings/__pycache__/crypto_hash.cpython-310.pyc,,
|
||||
nacl/bindings/__pycache__/crypto_kx.cpython-310.pyc,,
|
||||
nacl/bindings/__pycache__/crypto_pwhash.cpython-310.pyc,,
|
||||
nacl/bindings/__pycache__/crypto_scalarmult.cpython-310.pyc,,
|
||||
nacl/bindings/__pycache__/crypto_secretbox.cpython-310.pyc,,
|
||||
nacl/bindings/__pycache__/crypto_secretstream.cpython-310.pyc,,
|
||||
nacl/bindings/__pycache__/crypto_shorthash.cpython-310.pyc,,
|
||||
nacl/bindings/__pycache__/crypto_sign.cpython-310.pyc,,
|
||||
nacl/bindings/__pycache__/randombytes.cpython-310.pyc,,
|
||||
nacl/bindings/__pycache__/sodium_core.cpython-310.pyc,,
|
||||
nacl/bindings/__pycache__/utils.cpython-310.pyc,,
|
||||
nacl/bindings/crypto_aead.py,sha256=BIw1k_JCfr5ylZk0RF5rCFIM1fhfLkEa-aiWkrfffNE,15597
|
||||
nacl/bindings/crypto_box.py,sha256=Ox0NG2t4MsGhBAa7Kgah4o0gc99ULMsqkdX56ofOouY,10139
|
||||
nacl/bindings/crypto_core.py,sha256=6u9G3y7H-QrawO785UkFFFtwDoCkeHE63GOUl9p5-eA,13736
|
||||
nacl/bindings/crypto_generichash.py,sha256=9mX0DGIIzicr-uXrqFM1nU4tirasbixDwbcdfV7W1fc,8852
|
||||
nacl/bindings/crypto_hash.py,sha256=Rg1rsEwE3azhsQT-dNVPA4NB9VogJAKn1EfxYt0pPe0,2175
|
||||
nacl/bindings/crypto_kx.py,sha256=oZNVlNgROpHOa1XQ_uZe0tqIkdfuApeJlRnwR23_74k,6723
|
||||
nacl/bindings/crypto_pwhash.py,sha256=laVDo4xFUuGyEjtZAU510AklBF6ablBy7Z3HN1WDYjY,18848
|
||||
nacl/bindings/crypto_scalarmult.py,sha256=_DX-mst2uCnzjo6fP5HRTnhv1BC95B9gmJc3L_or16g,8244
|
||||
nacl/bindings/crypto_secretbox.py,sha256=KgZ1VvkCJDlQ85jtfe9c02VofPvuEgZEhWni-aX3MsM,2914
|
||||
nacl/bindings/crypto_secretstream.py,sha256=G0FgZS01qA5RzWzm5Bdms8Yy_lvgdZDoUYYBActPmvQ,11165
|
||||
nacl/bindings/crypto_shorthash.py,sha256=PQU7djHTLDGdVs-w_TsivjFHHp5EK5k2Yh6p-6z0T60,2603
|
||||
nacl/bindings/crypto_sign.py,sha256=53j2im9E4F79qT_2U8IfCAc3lzg0VMwEjvAPEUccVDg,10342
|
||||
nacl/bindings/randombytes.py,sha256=uBK3W4WcjgnjZdWanrX0fjYZpr9KHbBgNMl9rui-Ojc,1563
|
||||
nacl/bindings/sodium_core.py,sha256=9Y9CX--sq-TaPaQRPRpx8SWDSS9PJOja_Cqb-yqyJNQ,1039
|
||||
nacl/bindings/utils.py,sha256=KDwQnadXeNMbqEA1SmpNyCVo5k8MiUQa07QM66VzfXM,4298
|
||||
nacl/encoding.py,sha256=qTAPc2MXSkdh4cqDVY0ra6kHyViHMCmEo_re7cgGk5w,2915
|
||||
nacl/exceptions.py,sha256=GZH32aJtZgqCO4uz0LRsev8z0WyvAYuV3YVqT9AAQq4,2451
|
||||
nacl/hash.py,sha256=EYBOe6UVc9SUQINEmyuRSa1QGRSvdwdrBzTL1tdFLU8,6392
|
||||
nacl/hashlib.py,sha256=L5Fv75St8AMPvb-GhA4YqX5p1mC_Sb4HhC1NxNQMpJA,4400
|
||||
nacl/public.py,sha256=RVGCWQRjIJOmW-8sNrVLtsDjMMGx30i6UyfViGCnQNA,14792
|
||||
nacl/pwhash/__init__.py,sha256=XSDXd7wQHNLEHl0mkHfVb5lFQsp6ygHkhen718h0BSM,2675
|
||||
nacl/pwhash/__pycache__/__init__.cpython-310.pyc,,
|
||||
nacl/pwhash/__pycache__/_argon2.cpython-310.pyc,,
|
||||
nacl/pwhash/__pycache__/argon2i.cpython-310.pyc,,
|
||||
nacl/pwhash/__pycache__/argon2id.cpython-310.pyc,,
|
||||
nacl/pwhash/__pycache__/scrypt.cpython-310.pyc,,
|
||||
nacl/pwhash/_argon2.py,sha256=jL1ChR9biwYh3RSuc-LJ2-W4DlVLHpir-XHGX8cpeJQ,1779
|
||||
nacl/pwhash/argon2i.py,sha256=IIvIuO9siKUu5-Wpz0SGiltLQv7Du_mi9BUE8INRK_4,4405
|
||||
nacl/pwhash/argon2id.py,sha256=H22i8O4j9Ws4L3JsXl9TRcJzDcyaVumhQRPzINAgJWM,4433
|
||||
nacl/pwhash/scrypt.py,sha256=fMr3Qht1a1EY8aebNNntfLRjinIPXtKYKKrrBhY5LDc,6986
|
||||
nacl/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
nacl/secret.py,sha256=kauBNuP-0rb3TjU2EMBMu5Vnmzjnscp1bRqMspy5LzU,12108
|
||||
nacl/signing.py,sha256=kbTEUyHLUMaNLv1nCjxzGxCs82Qs5w8gxE_CnEwPuIU,8337
|
||||
nacl/utils.py,sha256=gmlTD1x9ZNwzHd8LpALH1CHud-Htv8ejRb3y7TyS9f0,2341
|
||||
@@ -0,0 +1,7 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: bdist_wheel (0.37.1)
|
||||
Root-Is-Purelib: false
|
||||
Tag: cp36-abi3-manylinux_2_17_x86_64
|
||||
Tag: cp36-abi3-manylinux2014_x86_64
|
||||
Tag: cp36-abi3-manylinux_2_24_x86_64
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
_sodium
|
||||
nacl
|
||||
BIN
venv/lib/python3.10/site-packages/_cffi_backend.cpython-310-x86_64-linux-gnu.so
Executable file
BIN
venv/lib/python3.10/site-packages/_cffi_backend.cpython-310-x86_64-linux-gnu.so
Executable file
Binary file not shown.
132
venv/lib/python3.10/site-packages/_distutils_hack/__init__.py
Normal file
132
venv/lib/python3.10/site-packages/_distutils_hack/__init__.py
Normal file
@@ -0,0 +1,132 @@
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
import importlib
|
||||
import warnings
|
||||
|
||||
|
||||
is_pypy = '__pypy__' in sys.builtin_module_names
|
||||
|
||||
|
||||
warnings.filterwarnings('ignore',
|
||||
r'.+ distutils\b.+ deprecated',
|
||||
DeprecationWarning)
|
||||
|
||||
|
||||
def warn_distutils_present():
|
||||
if 'distutils' not in sys.modules:
|
||||
return
|
||||
if is_pypy and sys.version_info < (3, 7):
|
||||
# PyPy for 3.6 unconditionally imports distutils, so bypass the warning
|
||||
# https://foss.heptapod.net/pypy/pypy/-/blob/be829135bc0d758997b3566062999ee8b23872b4/lib-python/3/site.py#L250
|
||||
return
|
||||
warnings.warn(
|
||||
"Distutils was imported before Setuptools, but importing Setuptools "
|
||||
"also replaces the `distutils` module in `sys.modules`. This may lead "
|
||||
"to undesirable behaviors or errors. To avoid these issues, avoid "
|
||||
"using distutils directly, ensure that setuptools is installed in the "
|
||||
"traditional way (e.g. not an editable install), and/or make sure "
|
||||
"that setuptools is always imported before distutils.")
|
||||
|
||||
|
||||
def clear_distutils():
|
||||
if 'distutils' not in sys.modules:
|
||||
return
|
||||
warnings.warn("Setuptools is replacing distutils.")
|
||||
mods = [name for name in sys.modules if re.match(r'distutils\b', name)]
|
||||
for name in mods:
|
||||
del sys.modules[name]
|
||||
|
||||
|
||||
def enabled():
|
||||
"""
|
||||
Allow selection of distutils by environment variable.
|
||||
"""
|
||||
which = os.environ.get('SETUPTOOLS_USE_DISTUTILS', 'stdlib')
|
||||
return which == 'local'
|
||||
|
||||
|
||||
def ensure_local_distutils():
|
||||
clear_distutils()
|
||||
|
||||
# With the DistutilsMetaFinder in place,
|
||||
# perform an import to cause distutils to be
|
||||
# loaded from setuptools._distutils. Ref #2906.
|
||||
add_shim()
|
||||
importlib.import_module('distutils')
|
||||
remove_shim()
|
||||
|
||||
# check that submodules load as expected
|
||||
core = importlib.import_module('distutils.core')
|
||||
assert '_distutils' in core.__file__, core.__file__
|
||||
|
||||
|
||||
def do_override():
|
||||
"""
|
||||
Ensure that the local copy of distutils is preferred over stdlib.
|
||||
|
||||
See https://github.com/pypa/setuptools/issues/417#issuecomment-392298401
|
||||
for more motivation.
|
||||
"""
|
||||
if enabled():
|
||||
warn_distutils_present()
|
||||
ensure_local_distutils()
|
||||
|
||||
|
||||
class DistutilsMetaFinder:
|
||||
def find_spec(self, fullname, path, target=None):
|
||||
if path is not None:
|
||||
return
|
||||
|
||||
method_name = 'spec_for_{fullname}'.format(**locals())
|
||||
method = getattr(self, method_name, lambda: None)
|
||||
return method()
|
||||
|
||||
def spec_for_distutils(self):
|
||||
import importlib.abc
|
||||
import importlib.util
|
||||
|
||||
class DistutilsLoader(importlib.abc.Loader):
|
||||
|
||||
def create_module(self, spec):
|
||||
return importlib.import_module('setuptools._distutils')
|
||||
|
||||
def exec_module(self, module):
|
||||
pass
|
||||
|
||||
return importlib.util.spec_from_loader('distutils', DistutilsLoader())
|
||||
|
||||
def spec_for_pip(self):
|
||||
"""
|
||||
Ensure stdlib distutils when running under pip.
|
||||
See pypa/pip#8761 for rationale.
|
||||
"""
|
||||
if self.pip_imported_during_build():
|
||||
return
|
||||
clear_distutils()
|
||||
self.spec_for_distutils = lambda: None
|
||||
|
||||
@staticmethod
|
||||
def pip_imported_during_build():
|
||||
"""
|
||||
Detect if pip is being imported in a build script. Ref #2355.
|
||||
"""
|
||||
import traceback
|
||||
return any(
|
||||
frame.f_globals['__file__'].endswith('setup.py')
|
||||
for frame, line in traceback.walk_stack(None)
|
||||
)
|
||||
|
||||
|
||||
DISTUTILS_FINDER = DistutilsMetaFinder()
|
||||
|
||||
|
||||
def add_shim():
|
||||
sys.meta_path.insert(0, DISTUTILS_FINDER)
|
||||
|
||||
|
||||
def remove_shim():
|
||||
try:
|
||||
sys.meta_path.remove(DISTUTILS_FINDER)
|
||||
except ValueError:
|
||||
pass
|
||||
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user