Initial commit of AthonetTools

This commit is contained in:
2025-08-21 12:59:43 +00:00
commit cd932b8fcb
2483 changed files with 433999 additions and 0 deletions

90
Triton.conf Normal file
View 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>

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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 }

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View 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
View 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
View 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
View 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
View 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
View 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
View 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)

0
hello.py Normal file
View File

5
requirements.txt Normal file
View File

@@ -0,0 +1,5 @@
# requirements.txt
Flask
requests
gunicorn
paramiko

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,2 @@
from flask import Blueprint
bp = Blueprint("cc_v1", __name__)

25
services/api/cc_v1/ncm.py Normal file
View 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
View 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
View 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

View 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
View 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

View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1 @@
{"peers":[],"current_node":{"name":"JohnWayne","node":"pls@127.0.0.1","api_address":"192.168.86.54"},"unreachable_peers":[]}

View 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": "" } }
]
}
}

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 490 KiB

View 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
View 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
View 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" }
}

View 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 || '' };
}

View 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}`);
}
});
}

View 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
View 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
View 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"
}
}

View 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

View 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 }}

View 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 }}

View 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"

View 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 }

View 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
View 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
View 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>

View 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 &amp; 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
View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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
View 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 %}

View 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 %}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1 @@
python3

1
venv/bin/python3 Symbolic link
View File

@@ -0,0 +1 @@
/usr/bin/python3

1
venv/bin/python3.10 Symbolic link
View File

@@ -0,0 +1 @@
python3

View File

@@ -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.

View File

@@ -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('&lt;script&gt;alert(document.cookie);&lt;/script&gt;')
>>> # 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>&#34;World&#34;</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

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1 @@
markupsafe

View File

@@ -0,0 +1 @@
pip

View 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.

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,2 @@
_sodium
nacl

View 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

Some files were not shown because too many files have changed in this diff Show More