Initial commit of AthonetTools
This commit is contained in:
+90
@@ -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.
Binary file not shown.
@@ -0,0 +1,12 @@
|
|||||||
|
vswitches:
|
||||||
|
- vSwitchName: GAF_VSWITCH
|
||||||
|
vSwitchNics: [vmnic4, vmnic5]
|
||||||
|
vSwitchSecurity:
|
||||||
|
forged_transmits: true
|
||||||
|
mac_changes: true
|
||||||
|
|
||||||
|
portgroups:
|
||||||
|
- { vSwitch: GAF_VSWITCH, vlanId: 501, vlanName: GAF_BP_501_OAM }
|
||||||
|
- { vSwitch: GAF_VSWITCH, vlanId: 502, vlanName: GAF_BP_502_RAN }
|
||||||
|
- { vSwitch: GAF_VSWITCH, vlanId: 10, vlanName: DN_01 }
|
||||||
|
- { vSwitch: GAF_VSWITCH, vlanId: 4095, vlanName: GAF_BP_T_510_515 }
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# 3GPP core identifiers / names
|
||||||
|
mcc: "315"
|
||||||
|
mnc: "010"
|
||||||
|
full_network_name: "JohnWayne"
|
||||||
|
short_network_name: "JohnWayne"
|
||||||
|
|
||||||
|
# AMF / GUAMI
|
||||||
|
amf_name: "amf01.5gc.3gppnetwork.org"
|
||||||
|
guami:
|
||||||
|
region: "02"
|
||||||
|
set: "003"
|
||||||
|
pointer: "000001"
|
||||||
|
|
||||||
|
# MME (for 4G interop / S1)
|
||||||
|
mme_name: "mme1"
|
||||||
|
mmegi: "0001"
|
||||||
|
mmec: "01"
|
||||||
|
mme_cname: "gw01.nodes"
|
||||||
|
|
||||||
|
# DNS info
|
||||||
|
epc_dns_zone_data:
|
||||||
|
# Additional PLMNs to handle
|
||||||
|
plmns:
|
||||||
|
- { mcc: '999', mnc: '99' }
|
||||||
|
- { mcc: '001', mnc: '01' }
|
||||||
|
- { mcc: '315', mnc: '010' }
|
||||||
|
|
||||||
|
# SBI configuration
|
||||||
|
sbi:
|
||||||
|
interface: lo
|
||||||
|
base_address: 127.0.1.1/24
|
||||||
|
|
||||||
|
# Transports configuration
|
||||||
|
_ngc_ext_aio_transport:
|
||||||
|
|
||||||
|
# AIO local transports
|
||||||
|
- action: set_local_transports
|
||||||
|
params: {}
|
||||||
|
|
||||||
|
# RAN transports (use RAN IP)
|
||||||
|
- action: override_amf_n2_transport
|
||||||
|
params: { address: 192.168.120.95, vrf: RAN }
|
||||||
|
- action: override_mme_transport
|
||||||
|
params: { s1_address: 192.168.120.95, s1_vrf: RAN }
|
||||||
|
|
||||||
|
# UPF transports (N3 on RAN)
|
||||||
|
- action: override_upf_transport
|
||||||
|
params:
|
||||||
|
n3_interface: eth1
|
||||||
|
n3_address: 192.168.120.95
|
||||||
|
n3_vrf: RAN
|
||||||
|
|
||||||
|
# DN/DNN (N6) with UE pool
|
||||||
|
- action: add_n6_dnn
|
||||||
|
params:
|
||||||
|
n6_dnn: internet
|
||||||
|
n6_vrf: DN_01
|
||||||
|
n6_vlan: 10
|
||||||
|
n6_vrf_table: 511
|
||||||
|
n6_interface: eth2
|
||||||
|
n6_ip: 192.168.110.95/24
|
||||||
|
n6_gw: 192.168.110.1
|
||||||
|
n6_upf_pools:
|
||||||
|
- upf_route: 100.0.94.0/24
|
||||||
|
nssai: false
|
||||||
|
n6_bgp:
|
||||||
|
local_as: 65001
|
||||||
|
peer_as: 65000
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
kind: ngcore-AIO
|
||||||
|
nf_skip_list:
|
||||||
|
- "aaa"
|
||||||
|
- "chf"
|
||||||
|
- "bmsc"
|
||||||
|
- "dra"
|
||||||
|
- "eir"
|
||||||
|
version: '25.1'
|
||||||
|
ova_file: /home/mjensen/OVA/HPE_ANW_P5G_Core-1.25.1.1-qemux86-64.ova
|
||||||
|
report_services: false
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# --- Networking recipe ---
|
||||||
|
net_recipe: generic_bgp
|
||||||
|
|
||||||
|
# --- OAM config ---
|
||||||
|
oam_network:
|
||||||
|
add_ansible_host_address: false
|
||||||
|
addresses:
|
||||||
|
- 192.168.105.159/24
|
||||||
|
gateway4: 192.168.105.1
|
||||||
|
|
||||||
|
# --- NTP ---
|
||||||
|
ntp:
|
||||||
|
- 0.pool.ntp.org
|
||||||
|
- 1.pool.ntp.org
|
||||||
|
|
||||||
|
# --- VRF config ---
|
||||||
|
_ngc_ext_aio_vrf:
|
||||||
|
- action: net_add_vrf
|
||||||
|
params: { name: RAN, table: 502 }
|
||||||
|
- action: net_add_vrf
|
||||||
|
params: { name: TELCO, table: 535 }
|
||||||
|
|
||||||
|
_ngc_ext_aio_net:
|
||||||
|
# RAN interface
|
||||||
|
- action: net_set_interface
|
||||||
|
params:
|
||||||
|
interface: eth1
|
||||||
|
vrf: RAN
|
||||||
|
addresses:
|
||||||
|
- 192.168.120.95/24 # S1+N2+N3
|
||||||
|
routes:
|
||||||
|
- destination: 0.0.0.0/0
|
||||||
|
gateway: 192.168.120.1
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
## UDM/UDR testing profile
|
||||||
|
create_testing_profile:
|
||||||
|
slices:
|
||||||
|
- { sst: 1, sd: '000001' }
|
||||||
|
- { sst: 1, sd: '' }
|
||||||
|
dnns:
|
||||||
|
- internet
|
||||||
|
plmns:
|
||||||
|
- { mcc: '315', mnc: '010' }
|
||||||
|
|
||||||
|
# UDR Subscribers provisioning
|
||||||
|
udr_provisioning:
|
||||||
|
testing_profile_service_profile: "testing_profile"
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
@@ -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
@@ -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
|
||||||
@@ -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":""}}
|
||||||
@@ -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,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.
@@ -0,0 +1,2 @@
|
|||||||
|
from flask import Blueprint
|
||||||
|
bp = Blueprint("cc_v1", __name__)
|
||||||
@@ -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})
|
||||||
@@ -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})
|
||||||
@@ -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
|
||||||
@@ -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()
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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}
|
||||||
@@ -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)
|
||||||
@@ -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 {}
|
||||||
@@ -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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"peers":[],"current_node":{"name":"JohnWayne","node":"pls@127.0.0.1","api_address":"192.168.86.54"},"unreachable_peers":[]}
|
||||||
@@ -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": "" } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 490 KiB |
@@ -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;
|
||||||
|
}
|
||||||
@@ -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'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
@@ -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" }
|
||||||
|
}
|
||||||
@@ -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 || '' };
|
||||||
|
}
|
||||||
@@ -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}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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 }}
|
||||||
@@ -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 }}
|
||||||
@@ -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"
|
||||||
@@ -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 }
|
||||||
@@ -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 }}
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,431 @@
|
|||||||
|
{% extends "layout.html" %}
|
||||||
|
{% set active_page = 'gaf_desk' %}
|
||||||
|
{% block title %}GAF Desk - {{ super() }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h2 id="page-title" class="mb-0">GAF Desk: Configuration Generator</h2>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label for="blueprint-select" class="form-label"><h5>1. Select a Blueprint</h5></label>
|
||||||
|
<select class="form-select" id="blueprint-select">
|
||||||
|
<option selected value="">Choose a blueprint...</option>
|
||||||
|
<option value="00_simple_5G_only">00_simple_5G_only</option>
|
||||||
|
<option value="01_single_site">01_single_site</option>
|
||||||
|
<option value="02_high_availability">02_high_availability</option>
|
||||||
|
<option value="03_distributed">03_distributed</option>
|
||||||
|
<option value="04_high_availability_ipv6">04_high_availability_ipv6</option>
|
||||||
|
<option value="05_high_availability_ospf">05_high_availability_ospf</option>
|
||||||
|
<option value="06_upf_active_standby">06_upf_active_standby</option>
|
||||||
|
<option value="07_high_availability_proxmox">07_high_availability_proxmox</option>
|
||||||
|
<option value="08_all_in_one">08_all_in_one</option>
|
||||||
|
<option value="10_4G_MVNO">10_4G_MVNO</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card my-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5>2. Global Configuration</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label for="network-name-input" class="form-label">Network Name</label>
|
||||||
|
<input type="text" class="form-control" id="network-name-input" value="JohnWayne" readonly
|
||||||
|
data-bs-toggle="tooltip" title="This is the hardcoded network name.">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label for="plmn-input" class="form-label">PLMN</label>
|
||||||
|
<input type="text" class="form-control" id="plmn-input" value="315-010" readonly
|
||||||
|
data-bs-toggle="tooltip" title="The hardcoded PLMN ID for this configuration.">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label for="dns-input" class="form-label">DNS</label>
|
||||||
|
<input type="text" class="form-control" id="dns-input" value="8.8.8.8" readonly
|
||||||
|
data-bs-toggle="tooltip" title="Primary DNS server. This value is fixed.">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label for="ntp-input" class="form-label">NTP</label>
|
||||||
|
<input type="text" class="form-control" id="ntp-input" value="0.pool.ntp.org, 1.pool.ntp.org" readonly
|
||||||
|
data-bs-toggle="tooltip" title="NTP servers for time synchronization.">
|
||||||
|
</div>
|
||||||
|
<div class="row g-3 mt-2">
|
||||||
|
<!-- RAN -->
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="mb-1"><span class="form-label d-block fw-semibold">RAN Network</span></div>
|
||||||
|
|
||||||
|
<input type="text" class="form-control" id="ip-core-ran"
|
||||||
|
placeholder="Core IP addr in format 172.28.20.25/24"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
title="Enter the IP range for the connection between the Cell Site and the Core.">
|
||||||
|
|
||||||
|
<input type="text" class="form-control mt-2" id="ip-core-ran-gw"
|
||||||
|
placeholder="RAN Gateway IP e.g. 172.28.20.1">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="mb-1"><span class="form-label d-block fw-semibold">Management Network</span></div>
|
||||||
|
|
||||||
|
<input type="text" class="form-control" id="ip-core-mgmt"
|
||||||
|
value="via DHCP" readonly
|
||||||
|
placeholder="Will be set in Step 1"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
title="This IP will be set automatically based on the current DHCP address during prechecks.">
|
||||||
|
|
||||||
|
<input type="text" class="form-control mt-2" id="ip-core-mgmt-gw"
|
||||||
|
value="via DHCP" readonly
|
||||||
|
placeholder="Gateway will be set in Step 1"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
title="This gateway will be set automatically based on the current DHCP address during prechecks.">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="mb-1"><span class="form-label d-block fw-semibold">Data Network</span></div>
|
||||||
|
|
||||||
|
<input type="text" class="form-control" id="ip-core-dn"
|
||||||
|
placeholder="Core DN/APN IP addr in format 172.28.10.25/24"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
title="Enter the IP range for the connection between the Core and the Data Network.">
|
||||||
|
|
||||||
|
<input type="text" class="form-control mt-2" id="ip-core-dn-gw"
|
||||||
|
placeholder="DN/APN Gateway IP e.g. 172.28.20.1">
|
||||||
|
|
||||||
|
<label for="vlan-core-dn" class="visually-hidden">Data Network VLAN</label>
|
||||||
|
<input type="number" class="form-control mt-2" id="ip-core-dn-vlan"
|
||||||
|
placeholder="VLAN (e.g. 200)" min="1" max="4094">
|
||||||
|
|
||||||
|
<label for="ue-ip-pool" class="visually-hidden">UE IP Pool (CIDR)</label>
|
||||||
|
<input type="text" class="form-control mt-2" id="ip-core-dn-uepool"
|
||||||
|
placeholder="UE IP Pool (e.g. 10.20.0.0/16)">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5>3. Blueprint Details</h5>
|
||||||
|
|
||||||
|
<div id="blueprint-diagram-area" class="mt-3">
|
||||||
|
<div id="cy-5g" class="d-none" style="height:520px; border:1px solid #e5e7eb; border-radius:8px;"></div>
|
||||||
|
|
||||||
|
<div id="diagram-empty" class="text-muted text-center py-4" aria-live="polite">
|
||||||
|
Please select a blueprint to see its diagram.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div id="deployment-wizard">
|
||||||
|
<h5 class="mb-3">4. Deployment Wizard</h5>
|
||||||
|
|
||||||
|
<div class="progress mb-4" style="height: 25px;">
|
||||||
|
<div class="progress-bar" role="progressbar" style="width: 0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">0%</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="accordion" id="deploymentAccordion">
|
||||||
|
|
||||||
|
<div class="accordion-item">
|
||||||
|
<h2 class="accordion-header" id="headingOne">
|
||||||
|
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseOne">
|
||||||
|
Stage 1: Prechecks
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="collapseOne" class="accordion-collapse collapse show" data-bs-parent="#deploymentAccordion">
|
||||||
|
<div class="accordion-body">
|
||||||
|
<ul class="list-group">
|
||||||
|
<li class="list-group-item" id="target-host-row">
|
||||||
|
<div class="d-flex flex-wrap align-items-center gap-2">
|
||||||
|
<strong class="me-2">Target host (VPN IP):</strong>
|
||||||
|
<div class="input-group" style="max-width: 260px;">
|
||||||
|
<span class="input-group-text">IPv4</span>
|
||||||
|
<input id="target-ip-input" type="text" class="form-control" placeholder="10.x.x.x" inputmode="numeric" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
<button id="btn-enable-access" class="btn btn-sm btn-primary">Enable Access</button>
|
||||||
|
<span id="access-badge" class="badge bg-secondary ms-2">Pending</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-wrap align-items-center gap-2 mt-2">
|
||||||
|
<strong class="me-2">Retrieve eth0 network info:</strong>
|
||||||
|
<button class="btn btn-sm btn-primary" id="btn-capture-oam">Run</button>
|
||||||
|
<span class="badge bg-secondary ms-2" id="oam-badge">Pending</span>
|
||||||
|
</div>
|
||||||
|
<div class="small text-muted mt-2">
|
||||||
|
Enables SSH & Webconsole (enable → enable-autostart → start).<br>
|
||||||
|
Retrieves eth0 network info and configures static address.
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="accordion-item">
|
||||||
|
<h2 class="accordion-header" id="headingTwo">
|
||||||
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseTwo">
|
||||||
|
Stage 2: Preparation
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="collapseTwo" class="accordion-collapse collapse" data-bs-parent="#deploymentAccordion">
|
||||||
|
<div class="accordion-body">
|
||||||
|
<ul class="list-group">
|
||||||
|
<li class="list-group-item" id="create-yaml-row">
|
||||||
|
<div class="d-flex flex-wrap align-items-center gap-2">
|
||||||
|
<strong class="me-2">Create YAML files:</strong>
|
||||||
|
<button class="btn btn-sm btn-primary" id="btn-create-yaml">Create</button>
|
||||||
|
<span class="badge bg-secondary ms-2" id="yaml-badge">Pending</span>
|
||||||
|
</div>
|
||||||
|
<div class="small text-muted mt-2">
|
||||||
|
Generates YAML files for deployment based on the configuration above.
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="accordion-item">
|
||||||
|
<h2 class="accordion-header" id="headingThree">
|
||||||
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseThree">
|
||||||
|
Stage 3: Execution
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="collapseThree" class="accordion-collapse collapse" data-bs-parent="#deploymentAccordion">
|
||||||
|
<div class="accordion-body">
|
||||||
|
<ul class="list-group">
|
||||||
|
<li class="list-group-item" id="run-gaf-row">
|
||||||
|
<div class="d-flex flex-wrap align-items-center gap-2">
|
||||||
|
<strong class="me-2">Run GAF:</strong>
|
||||||
|
<button class="btn btn-sm btn-primary" id="btn-run-gaf">Run</button>
|
||||||
|
</div>
|
||||||
|
<div class="small text-muted mt-2">
|
||||||
|
Executes the GAF deployment process.
|
||||||
|
</div>
|
||||||
|
<div class="mt-3">
|
||||||
|
<pre id="gaf-output" style="background:#f8f9fa; border-radius:6px; padding:12px; max-height:300px; overflow:auto;"></pre>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="accordion-item">
|
||||||
|
<h2 class="accordion-header" id="headingFour">
|
||||||
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseFour">
|
||||||
|
Stage 4: Postchecks
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="collapseFour" class="accordion-collapse collapse" data-bs-parent="#deploymentAccordion">
|
||||||
|
<div class="accordion-body">
|
||||||
|
<ul class="list-group">
|
||||||
|
<li class="list-group-item" id="post-backup-row">
|
||||||
|
<div class="d-flex flex-wrap align-items-center gap-2">
|
||||||
|
<strong class="me-2">Post-backup:</strong>
|
||||||
|
<button class="btn btn-sm btn-primary" id="btn-post-backup" disabled>Run</button>
|
||||||
|
</div>
|
||||||
|
<div class="small text-muted mt-2">
|
||||||
|
Performs post-deployment backup operations.
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_scripts %}
|
||||||
|
<script src="https://unpkg.com/cytoscape@3.28.0/dist/cytoscape.min.js"></script>
|
||||||
|
<script src="https://unpkg.com/dagre@0.8.5/dist/dagre.min.js"></script>
|
||||||
|
<script src="https://unpkg.com/cytoscape-dagre@2.5.0/cytoscape-dagre.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
||||||
|
[...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
import { mountCy } from '/static/js/graph/initGraph.js';
|
||||||
|
import { mountWizard } from '/static/js/wizard/steps.js';
|
||||||
|
import { mountStep3Deploy } from '/static/js/wizard/step3_deploy.js';
|
||||||
|
mountWizard();
|
||||||
|
mountStep3Deploy();
|
||||||
|
|
||||||
|
// === Recapture OAM (eth0) on demand ===
|
||||||
|
|
||||||
|
// Stage 3 deployment logic is now handled by static/js/wizard/step3_deploy.js
|
||||||
|
async function recaptureOAM(host) {
|
||||||
|
const oamBadge = document.getElementById('oam-badge');
|
||||||
|
const mgmtInput = document.getElementById('ip-core-mgmt'); // the read-only field in "Management Network"
|
||||||
|
|
||||||
|
if (!host) {
|
||||||
|
alert('Enter the Target host first (Stage 1).');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
oamBadge.textContent = 'Running…';
|
||||||
|
oamBadge.className = 'badge bg-warning';
|
||||||
|
|
||||||
|
const res = await fetch('/api/local/eth0/capture', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ host })
|
||||||
|
});
|
||||||
|
const j = await res.json();
|
||||||
|
|
||||||
|
if (!res.ok || !j.ok) throw new Error(j.error || `HTTP ${res.status}`);
|
||||||
|
|
||||||
|
// Update UI with fresh values
|
||||||
|
oamBadge.textContent = 'Captured';
|
||||||
|
oamBadge.className = 'badge bg-success';
|
||||||
|
if (mgmtInput) mgmtInput.value = j.cidr; // e.g. 192.168.86.55/24
|
||||||
|
const mgmtGwInput = document.getElementById('ip-core-mgmt-gw');
|
||||||
|
if (mgmtGwInput) mgmtGwInput.value = j.gw || '';
|
||||||
|
|
||||||
|
return j; // { cidr, gw, ok:true }
|
||||||
|
} catch (e) {
|
||||||
|
console.error('OAM capture failed:', e);
|
||||||
|
oamBadge.textContent = 'Failed';
|
||||||
|
oamBadge.className = 'badge bg-danger';
|
||||||
|
alert(`OAM capture failed: ${e.message}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wire the Stage 1 "Run" button to always re-capture
|
||||||
|
document.getElementById('btn-capture-oam')?.addEventListener('click', async () => {
|
||||||
|
const targetIp = document.querySelector('#target-host-row input')?.value?.trim();
|
||||||
|
await recaptureOAM(targetIp);
|
||||||
|
});
|
||||||
|
|
||||||
|
const blueprintSelect = document.getElementById('blueprint-select');
|
||||||
|
const cyContainer = document.getElementById('cy-5g');
|
||||||
|
const emptyMsg = document.getElementById('diagram-empty');
|
||||||
|
|
||||||
|
let cy = null;
|
||||||
|
const cache = new Map();
|
||||||
|
|
||||||
|
function showEmpty() {
|
||||||
|
if (cy) { cy.destroy(); cy = null; }
|
||||||
|
cyContainer.classList.add('d-none');
|
||||||
|
emptyMsg.textContent = 'Please select a blueprint to see its diagram.'; // reset
|
||||||
|
emptyMsg.classList.remove('d-none');
|
||||||
|
}
|
||||||
|
|
||||||
|
let dagreRegistered = false;
|
||||||
|
|
||||||
|
async function loadBlueprint(name) {
|
||||||
|
if (!name) return showEmpty();
|
||||||
|
|
||||||
|
const url = `/static/blueprints/${name}.json`;
|
||||||
|
try {
|
||||||
|
const data = cache.has(url) ? cache.get(url) : await (await fetch(url, { cache: 'no-cache' })).json();
|
||||||
|
cache.set(url, data);
|
||||||
|
|
||||||
|
emptyMsg.classList.add('d-none');
|
||||||
|
cyContainer.classList.remove('d-none');
|
||||||
|
|
||||||
|
if (cy) { cy.destroy(); cy = null; }
|
||||||
|
if (!dagreRegistered && window.cytoscapeDagre) {
|
||||||
|
cytoscape.use(cytoscapeDagre);
|
||||||
|
dagreRegistered = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
cy = mountCy(cyContainer, data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load blueprint:', err);
|
||||||
|
showEmpty();
|
||||||
|
emptyMsg.textContent = 'Failed to load the selected blueprint.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
blueprintSelect.addEventListener('change', () => {
|
||||||
|
const selectedValue = blueprintSelect.value;
|
||||||
|
loadBlueprint(selectedValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle Create YAML button click (calls /api/ansible/render)
|
||||||
|
document.getElementById('btn-create-yaml').addEventListener('click', async () => {
|
||||||
|
const val = id => (document.getElementById(id)?.value || '').trim();
|
||||||
|
|
||||||
|
// Arrays
|
||||||
|
const dns = (val('dns-input') || '8.8.8.8')
|
||||||
|
.split(',')
|
||||||
|
.map(s => s.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
const ntp = (val('ntp-input') || '0.pool.ntp.org, 1.pool.ntp.org')
|
||||||
|
.split(',')
|
||||||
|
.map(s => s.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
// DN VLAN as number (or undefined)
|
||||||
|
const vlanStr = val('ip-core-dn-vlan');
|
||||||
|
const vlanNum = vlanStr ? Number(vlanStr) : undefined;
|
||||||
|
|
||||||
|
// Target IP from Stage 1
|
||||||
|
const targetIp = document.querySelector('#target-host-row input')?.value?.trim() || '';
|
||||||
|
|
||||||
|
// Always recapture just before render so we never use stale cache
|
||||||
|
const cap = await recaptureOAM(targetIp); // <- requires the helper added earlier
|
||||||
|
if (!cap) return; // capture failed; stop here
|
||||||
|
|
||||||
|
// Update the Management field from fresh capture (nice UX)
|
||||||
|
const mgmtField = document.getElementById('ip-core-mgmt');
|
||||||
|
if (mgmtField && cap.cidr) mgmtField.value = cap.cidr;
|
||||||
|
|
||||||
|
// Build payload; mgmt is determined server-side from the fresh capture
|
||||||
|
const payload = {
|
||||||
|
hostname: val('network-name-input') || 'AIO-1',
|
||||||
|
network_name: val('network-name-input') || 'Network',
|
||||||
|
plmn: val('plmn-input') || '315-010',
|
||||||
|
dns,
|
||||||
|
ntp,
|
||||||
|
ran: { cidr: val('ip-core-ran'), gw: val('ip-core-ran-gw') },
|
||||||
|
dn: {
|
||||||
|
cidr: val('ip-core-dn'),
|
||||||
|
gw: val('ip-core-dn-gw'),
|
||||||
|
vlan: vlanNum,
|
||||||
|
ue_pool: val('ip-core-dn-uepool'),
|
||||||
|
dnn: 'internet'
|
||||||
|
},
|
||||||
|
inventory_host: 'GBP08-AIO-1',
|
||||||
|
esxi_host: 'ESXI-1',
|
||||||
|
version: '25.1',
|
||||||
|
ova_file: '/home/mjensen/OVA/HPE_ANW_P5G_Core-1.25.1.1-qemux86-64.ova',
|
||||||
|
|
||||||
|
// Make sure backend knows which host to use; it will read the fresh snapshot
|
||||||
|
ansible_host_ip: targetIp,
|
||||||
|
force_oam_refresh: true // harmless hint; backend may ignore if not implemented
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/ansible/render', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
const j = await res.json();
|
||||||
|
if (!res.ok || !j.ok) throw new Error(j.error || `HTTP ${res.status}`);
|
||||||
|
|
||||||
|
document.getElementById('yaml-badge').textContent = 'Created';
|
||||||
|
document.getElementById('yaml-badge').className = 'badge bg-success';
|
||||||
|
alert(`YAML files created in: ${j.staging}`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error creating YAML:', err);
|
||||||
|
document.getElementById('yaml-badge').textContent = 'Failed';
|
||||||
|
document.getElementById('yaml-badge').className = 'badge bg-danger';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
showEmpty();
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -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 %}
|
||||||
@@ -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 %}
|
||||||
@@ -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 %}
|
||||||
@@ -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 %}
|
||||||
@@ -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 %}
|
||||||
@@ -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 %}
|
||||||
@@ -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 %}
|
||||||
@@ -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 %}
|
||||||
@@ -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 %}
|
||||||
@@ -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 %}
|
||||||
@@ -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 %}
|
||||||
@@ -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"
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
Executable
+8
@@ -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())
|
||||||
Executable
+8
@@ -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())
|
||||||
Executable
+8
@@ -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())
|
||||||
Executable
+8
@@ -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())
|
||||||
Executable
+8
@@ -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())
|
||||||
Executable
+8
@@ -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())
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
python3
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
/usr/bin/python3
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
python3
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pip
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
Copyright 2010 Pallets
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are
|
||||||
|
met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in the
|
||||||
|
documentation and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
3. Neither the name of the copyright holder nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
||||||
|
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||||
|
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||||
|
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||||
|
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
Metadata-Version: 2.1
|
||||||
|
Name: MarkupSafe
|
||||||
|
Version: 3.0.2
|
||||||
|
Summary: Safely add untrusted strings to HTML/XML markup.
|
||||||
|
Maintainer-email: Pallets <contact@palletsprojects.com>
|
||||||
|
License: Copyright 2010 Pallets
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are
|
||||||
|
met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in the
|
||||||
|
documentation and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
3. Neither the name of the copyright holder nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
||||||
|
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||||
|
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||||
|
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||||
|
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
Project-URL: Donate, https://palletsprojects.com/donate
|
||||||
|
Project-URL: Documentation, https://markupsafe.palletsprojects.com/
|
||||||
|
Project-URL: Changes, https://markupsafe.palletsprojects.com/changes/
|
||||||
|
Project-URL: Source, https://github.com/pallets/markupsafe/
|
||||||
|
Project-URL: Chat, https://discord.gg/pallets
|
||||||
|
Classifier: Development Status :: 5 - Production/Stable
|
||||||
|
Classifier: Environment :: Web Environment
|
||||||
|
Classifier: Intended Audience :: Developers
|
||||||
|
Classifier: License :: OSI Approved :: BSD License
|
||||||
|
Classifier: Operating System :: OS Independent
|
||||||
|
Classifier: Programming Language :: Python
|
||||||
|
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
||||||
|
Classifier: Topic :: Text Processing :: Markup :: HTML
|
||||||
|
Classifier: Typing :: Typed
|
||||||
|
Requires-Python: >=3.9
|
||||||
|
Description-Content-Type: text/markdown
|
||||||
|
License-File: LICENSE.txt
|
||||||
|
|
||||||
|
# MarkupSafe
|
||||||
|
|
||||||
|
MarkupSafe implements a text object that escapes characters so it is
|
||||||
|
safe to use in HTML and XML. Characters that have special meanings are
|
||||||
|
replaced so that they display as the actual characters. This mitigates
|
||||||
|
injection attacks, meaning untrusted user input can safely be displayed
|
||||||
|
on a page.
|
||||||
|
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
```pycon
|
||||||
|
>>> from markupsafe import Markup, escape
|
||||||
|
|
||||||
|
>>> # escape replaces special characters and wraps in Markup
|
||||||
|
>>> escape("<script>alert(document.cookie);</script>")
|
||||||
|
Markup('<script>alert(document.cookie);</script>')
|
||||||
|
|
||||||
|
>>> # wrap in Markup to mark text "safe" and prevent escaping
|
||||||
|
>>> Markup("<strong>Hello</strong>")
|
||||||
|
Markup('<strong>hello</strong>')
|
||||||
|
|
||||||
|
>>> escape(Markup("<strong>Hello</strong>"))
|
||||||
|
Markup('<strong>hello</strong>')
|
||||||
|
|
||||||
|
>>> # Markup is a str subclass
|
||||||
|
>>> # methods and operators escape their arguments
|
||||||
|
>>> template = Markup("Hello <em>{name}</em>")
|
||||||
|
>>> template.format(name='"World"')
|
||||||
|
Markup('Hello <em>"World"</em>')
|
||||||
|
```
|
||||||
|
|
||||||
|
## Donate
|
||||||
|
|
||||||
|
The Pallets organization develops and supports MarkupSafe and other
|
||||||
|
popular packages. In order to grow the community of contributors and
|
||||||
|
users, and allow the maintainers to devote more time to the projects,
|
||||||
|
[please donate today][].
|
||||||
|
|
||||||
|
[please donate today]: https://palletsprojects.com/donate
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
MarkupSafe-3.0.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||||
|
MarkupSafe-3.0.2.dist-info/LICENSE.txt,sha256=SJqOEQhQntmKN7uYPhHg9-HTHwvY-Zp5yESOf_N9B-o,1475
|
||||||
|
MarkupSafe-3.0.2.dist-info/METADATA,sha256=aAwbZhSmXdfFuMM-rEHpeiHRkBOGESyVLJIuwzHP-nw,3975
|
||||||
|
MarkupSafe-3.0.2.dist-info/RECORD,,
|
||||||
|
MarkupSafe-3.0.2.dist-info/WHEEL,sha256=_kVlewavvOSnwZE_whBk3jlE_Ob-nL5GvlVcLkpXSD8,151
|
||||||
|
MarkupSafe-3.0.2.dist-info/top_level.txt,sha256=qy0Plje5IJuvsCBjejJyhDCjEAdcDLK_2agVcex8Z6U,11
|
||||||
|
markupsafe/__init__.py,sha256=sr-U6_27DfaSrj5jnHYxWN-pvhM27sjlDplMDPZKm7k,13214
|
||||||
|
markupsafe/__pycache__/__init__.cpython-310.pyc,,
|
||||||
|
markupsafe/__pycache__/_native.cpython-310.pyc,,
|
||||||
|
markupsafe/_native.py,sha256=hSLs8Jmz5aqayuengJJ3kdT5PwNpBWpKrmQSdipndC8,210
|
||||||
|
markupsafe/_speedups.c,sha256=O7XulmTo-epI6n2FtMVOrJXl8EAaIwD2iNYmBI5SEoQ,4149
|
||||||
|
markupsafe/_speedups.cpython-310-x86_64-linux-gnu.so,sha256=x4RoxWgyqAEokk-AZrWvrLDxLE-dm-zZSZYV_gOiLJA,34976
|
||||||
|
markupsafe/_speedups.pyi,sha256=ENd1bYe7gbBUf2ywyYWOGUpnXOHNJ-cgTNqetlW8h5k,41
|
||||||
|
markupsafe/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
Wheel-Version: 1.0
|
||||||
|
Generator: setuptools (75.2.0)
|
||||||
|
Root-Is-Purelib: false
|
||||||
|
Tag: cp310-cp310-manylinux_2_17_x86_64
|
||||||
|
Tag: cp310-cp310-manylinux2014_x86_64
|
||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
markupsafe
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pip
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
@@ -0,0 +1,245 @@
|
|||||||
|
Metadata-Version: 2.1
|
||||||
|
Name: PyNaCl
|
||||||
|
Version: 1.5.0
|
||||||
|
Summary: Python binding to the Networking and Cryptography (NaCl) library
|
||||||
|
Home-page: https://github.com/pyca/pynacl/
|
||||||
|
Author: The PyNaCl developers
|
||||||
|
Author-email: cryptography-dev@python.org
|
||||||
|
License: Apache License 2.0
|
||||||
|
Platform: UNKNOWN
|
||||||
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
||||||
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
||||||
|
Classifier: Programming Language :: Python :: 3
|
||||||
|
Classifier: Programming Language :: Python :: 3.6
|
||||||
|
Classifier: Programming Language :: Python :: 3.7
|
||||||
|
Classifier: Programming Language :: Python :: 3.8
|
||||||
|
Classifier: Programming Language :: Python :: 3.9
|
||||||
|
Classifier: Programming Language :: Python :: 3.10
|
||||||
|
Requires-Python: >=3.6
|
||||||
|
Requires-Dist: cffi (>=1.4.1)
|
||||||
|
Provides-Extra: docs
|
||||||
|
Requires-Dist: sphinx (>=1.6.5) ; extra == 'docs'
|
||||||
|
Requires-Dist: sphinx-rtd-theme ; extra == 'docs'
|
||||||
|
Provides-Extra: tests
|
||||||
|
Requires-Dist: pytest (!=3.3.0,>=3.2.1) ; extra == 'tests'
|
||||||
|
Requires-Dist: hypothesis (>=3.27.0) ; extra == 'tests'
|
||||||
|
|
||||||
|
===============================================
|
||||||
|
PyNaCl: Python binding to the libsodium library
|
||||||
|
===============================================
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/pypi/v/pynacl.svg
|
||||||
|
:target: https://pypi.org/project/PyNaCl/
|
||||||
|
:alt: Latest Version
|
||||||
|
|
||||||
|
.. image:: https://codecov.io/github/pyca/pynacl/coverage.svg?branch=main
|
||||||
|
:target: https://codecov.io/github/pyca/pynacl?branch=main
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/pypi/pyversions/pynacl.svg
|
||||||
|
:target: https://pypi.org/project/PyNaCl/
|
||||||
|
:alt: Compatible Python Versions
|
||||||
|
|
||||||
|
PyNaCl is a Python binding to `libsodium`_, which is a fork of the
|
||||||
|
`Networking and Cryptography library`_. These libraries have a stated goal of
|
||||||
|
improving usability, security and speed. It supports Python 3.6+ as well as
|
||||||
|
PyPy 3.
|
||||||
|
|
||||||
|
.. _libsodium: https://github.com/jedisct1/libsodium
|
||||||
|
.. _Networking and Cryptography library: https://nacl.cr.yp.to/
|
||||||
|
|
||||||
|
Features
|
||||||
|
--------
|
||||||
|
|
||||||
|
* Digital signatures
|
||||||
|
* Secret-key encryption
|
||||||
|
* Public-key encryption
|
||||||
|
* Hashing and message authentication
|
||||||
|
* Password based key derivation and password hashing
|
||||||
|
|
||||||
|
`Changelog`_
|
||||||
|
------------
|
||||||
|
|
||||||
|
.. _Changelog: https://pynacl.readthedocs.io/en/stable/changelog/
|
||||||
|
|
||||||
|
Installation
|
||||||
|
============
|
||||||
|
|
||||||
|
Binary wheel install
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
PyNaCl ships as a binary wheel on macOS, Windows and Linux ``manylinux1`` [#many]_ ,
|
||||||
|
so all dependencies are included. Make sure you have an up-to-date pip
|
||||||
|
and run:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
$ pip install pynacl
|
||||||
|
|
||||||
|
Faster wheel build
|
||||||
|
------------------
|
||||||
|
|
||||||
|
You can define the environment variable ``LIBSODIUM_MAKE_ARGS`` to pass arguments to ``make``
|
||||||
|
and enable `parallelization`_:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
$ LIBSODIUM_MAKE_ARGS=-j4 pip install pynacl
|
||||||
|
|
||||||
|
Linux source build
|
||||||
|
------------------
|
||||||
|
|
||||||
|
PyNaCl relies on `libsodium`_, a portable C library. A copy is bundled
|
||||||
|
with PyNaCl so to install you can run:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
$ pip install pynacl
|
||||||
|
|
||||||
|
If you'd prefer to use the version of ``libsodium`` provided by your
|
||||||
|
distribution, you can disable the bundled copy during install by running:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
$ SODIUM_INSTALL=system pip install pynacl
|
||||||
|
|
||||||
|
.. warning:: Usage of the legacy ``easy_install`` command provided by setuptools
|
||||||
|
is generally discouraged, and is completely unsupported in PyNaCl's case.
|
||||||
|
|
||||||
|
.. _parallelization: https://www.gnu.org/software/make/manual/html_node/Parallel.html
|
||||||
|
|
||||||
|
.. _libsodium: https://github.com/jedisct1/libsodium
|
||||||
|
|
||||||
|
.. [#many] `manylinux1 wheels <https://www.python.org/dev/peps/pep-0513/>`_
|
||||||
|
are built on a baseline linux environment based on Centos 5.11
|
||||||
|
and should work on most x86 and x86_64 glibc based linux environments.
|
||||||
|
|
||||||
|
Changelog
|
||||||
|
=========
|
||||||
|
|
||||||
|
1.5.0 (2022-01-07)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
* **BACKWARDS INCOMPATIBLE:** Removed support for Python 2.7 and Python 3.5.
|
||||||
|
* **BACKWARDS INCOMPATIBLE:** We no longer distribute ``manylinux1``
|
||||||
|
wheels.
|
||||||
|
* Added ``manylinux2014``, ``manylinux_2_24``, ``musllinux``, and macOS
|
||||||
|
``universal2`` wheels (the latter supports macOS ``arm64``).
|
||||||
|
* Update ``libsodium`` to 1.0.18-stable (July 25, 2021 release).
|
||||||
|
* Add inline type hints.
|
||||||
|
|
||||||
|
1.4.0 (2020-05-25)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
* Update ``libsodium`` to 1.0.18.
|
||||||
|
* **BACKWARDS INCOMPATIBLE:** We no longer distribute 32-bit ``manylinux1``
|
||||||
|
wheels. Continuing to produce them was a maintenance burden.
|
||||||
|
* Added support for Python 3.8, and removed support for Python 3.4.
|
||||||
|
* Add low level bindings for extracting the seed and the public key
|
||||||
|
from crypto_sign_ed25519 secret key
|
||||||
|
* Add low level bindings for deterministic random generation.
|
||||||
|
* Add ``wheel`` and ``setuptools`` setup_requirements in ``setup.py`` (#485)
|
||||||
|
* Fix checks on very slow builders (#481, #495)
|
||||||
|
* Add low-level bindings to ed25519 arithmetic functions
|
||||||
|
* Update low-level blake2b state implementation
|
||||||
|
* Fix wrong short-input behavior of SealedBox.decrypt() (#517)
|
||||||
|
* Raise CryptPrefixError exception instead of InvalidkeyError when trying
|
||||||
|
to check a password against a verifier stored in a unknown format (#519)
|
||||||
|
* Add support for minimal builds of libsodium. Trying to call functions
|
||||||
|
not available in a minimal build will raise an UnavailableError
|
||||||
|
exception. To compile a minimal build of the bundled libsodium, set
|
||||||
|
the SODIUM_INSTALL_MINIMAL environment variable to any non-empty
|
||||||
|
string (e.g. ``SODIUM_INSTALL_MINIMAL=1``) for setup.
|
||||||
|
|
||||||
|
1.3.0 2018-09-26
|
||||||
|
----------------
|
||||||
|
|
||||||
|
* Added support for Python 3.7.
|
||||||
|
* Update ``libsodium`` to 1.0.16.
|
||||||
|
* Run and test all code examples in PyNaCl docs through sphinx's
|
||||||
|
doctest builder.
|
||||||
|
* Add low-level bindings for chacha20-poly1305 AEAD constructions.
|
||||||
|
* Add low-level bindings for the chacha20-poly1305 secretstream constructions.
|
||||||
|
* Add low-level bindings for ed25519ph pre-hashed signing construction.
|
||||||
|
* Add low-level bindings for constant-time increment and addition
|
||||||
|
on fixed-precision big integers represented as little-endian
|
||||||
|
byte sequences.
|
||||||
|
* Add low-level bindings for the ISO/IEC 7816-4 compatible padding API.
|
||||||
|
* Add low-level bindings for libsodium's crypto_kx... key exchange
|
||||||
|
construction.
|
||||||
|
* Set hypothesis deadline to None in tests/test_pwhash.py to avoid
|
||||||
|
incorrect test failures on slower processor architectures. GitHub
|
||||||
|
issue #370
|
||||||
|
|
||||||
|
1.2.1 - 2017-12-04
|
||||||
|
------------------
|
||||||
|
|
||||||
|
* Update hypothesis minimum allowed version.
|
||||||
|
* Infrastructure: add proper configuration for readthedocs builder
|
||||||
|
runtime environment.
|
||||||
|
|
||||||
|
1.2.0 - 2017-11-01
|
||||||
|
------------------
|
||||||
|
|
||||||
|
* Update ``libsodium`` to 1.0.15.
|
||||||
|
* Infrastructure: add jenkins support for automatic build of
|
||||||
|
``manylinux1`` binary wheels
|
||||||
|
* Added support for ``SealedBox`` construction.
|
||||||
|
* Added support for ``argon2i`` and ``argon2id`` password hashing constructs
|
||||||
|
and restructured high-level password hashing implementation to expose
|
||||||
|
the same interface for all hashers.
|
||||||
|
* Added support for 128 bit ``siphashx24`` variant of ``siphash24``.
|
||||||
|
* Added support for ``from_seed`` APIs for X25519 keypair generation.
|
||||||
|
* Dropped support for Python 3.3.
|
||||||
|
|
||||||
|
1.1.2 - 2017-03-31
|
||||||
|
------------------
|
||||||
|
|
||||||
|
* reorder link time library search path when using bundled
|
||||||
|
libsodium
|
||||||
|
|
||||||
|
1.1.1 - 2017-03-15
|
||||||
|
------------------
|
||||||
|
|
||||||
|
* Fixed a circular import bug in ``nacl.utils``.
|
||||||
|
|
||||||
|
1.1.0 - 2017-03-14
|
||||||
|
------------------
|
||||||
|
|
||||||
|
* Dropped support for Python 2.6.
|
||||||
|
* Added ``shared_key()`` method on ``Box``.
|
||||||
|
* You can now pass ``None`` to ``nonce`` when encrypting with ``Box`` or
|
||||||
|
``SecretBox`` and it will automatically generate a random nonce.
|
||||||
|
* Added support for ``siphash24``.
|
||||||
|
* Added support for ``blake2b``.
|
||||||
|
* Added support for ``scrypt``.
|
||||||
|
* Update ``libsodium`` to 1.0.11.
|
||||||
|
* Default to the bundled ``libsodium`` when compiling.
|
||||||
|
* All raised exceptions are defined mixing-in
|
||||||
|
``nacl.exceptions.CryptoError``
|
||||||
|
|
||||||
|
1.0.1 - 2016-01-24
|
||||||
|
------------------
|
||||||
|
|
||||||
|
* Fix an issue with absolute paths that prevented the creation of wheels.
|
||||||
|
|
||||||
|
1.0 - 2016-01-23
|
||||||
|
----------------
|
||||||
|
|
||||||
|
* PyNaCl has been ported to use the new APIs available in cffi 1.0+.
|
||||||
|
Due to this change we no longer support PyPy releases older than 2.6.
|
||||||
|
* Python 3.2 support has been dropped.
|
||||||
|
* Functions to convert between Ed25519 and Curve25519 keys have been added.
|
||||||
|
|
||||||
|
0.3.0 - 2015-03-04
|
||||||
|
------------------
|
||||||
|
|
||||||
|
* The low-level API (`nacl.c.*`) has been changed to match the
|
||||||
|
upstream NaCl C/C++ conventions (as well as those of other NaCl bindings).
|
||||||
|
The order of arguments and return values has changed significantly. To
|
||||||
|
avoid silent failures, `nacl.c` has been removed, and replaced with
|
||||||
|
`nacl.bindings` (with the new argument ordering). If you have code which
|
||||||
|
calls these functions (e.g. `nacl.c.crypto_box_keypair()`), you must review
|
||||||
|
the new docstrings and update your code/imports to match the new
|
||||||
|
conventions.
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
PyNaCl-1.5.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||||
|
PyNaCl-1.5.0.dist-info/LICENSE,sha256=0xdK1j5yHUydzLitQyCEiZLTFDabxGMZcgtYAskVP-k,9694
|
||||||
|
PyNaCl-1.5.0.dist-info/METADATA,sha256=OJaXCiHgNRywLY9cj3X2euddUPZ4dnyyqAQMU01X4j0,8634
|
||||||
|
PyNaCl-1.5.0.dist-info/RECORD,,
|
||||||
|
PyNaCl-1.5.0.dist-info/WHEEL,sha256=TIQeZFe3DwXBO5UGlCH1aKpf5Cx6FJLbIUqd-Sq2juI,185
|
||||||
|
PyNaCl-1.5.0.dist-info/top_level.txt,sha256=wfdEOI_G2RIzmzsMyhpqP17HUh6Jcqi99to9aHLEslo,13
|
||||||
|
nacl/__init__.py,sha256=0IUunzBT8_Jn0DUdHacBExOYeAEMggo8slkfjo7O0XM,1116
|
||||||
|
nacl/__pycache__/__init__.cpython-310.pyc,,
|
||||||
|
nacl/__pycache__/encoding.cpython-310.pyc,,
|
||||||
|
nacl/__pycache__/exceptions.cpython-310.pyc,,
|
||||||
|
nacl/__pycache__/hash.cpython-310.pyc,,
|
||||||
|
nacl/__pycache__/hashlib.cpython-310.pyc,,
|
||||||
|
nacl/__pycache__/public.cpython-310.pyc,,
|
||||||
|
nacl/__pycache__/secret.cpython-310.pyc,,
|
||||||
|
nacl/__pycache__/signing.cpython-310.pyc,,
|
||||||
|
nacl/__pycache__/utils.cpython-310.pyc,,
|
||||||
|
nacl/_sodium.abi3.so,sha256=uJ6RwSnbb9wO4esR0bVUqrfFHtBOGm34IQIdmaE1fGY,2740136
|
||||||
|
nacl/bindings/__init__.py,sha256=BDlStrds2EuUS4swOL4pnf92PWVS_CHRCptX3KhEX-s,16997
|
||||||
|
nacl/bindings/__pycache__/__init__.cpython-310.pyc,,
|
||||||
|
nacl/bindings/__pycache__/crypto_aead.cpython-310.pyc,,
|
||||||
|
nacl/bindings/__pycache__/crypto_box.cpython-310.pyc,,
|
||||||
|
nacl/bindings/__pycache__/crypto_core.cpython-310.pyc,,
|
||||||
|
nacl/bindings/__pycache__/crypto_generichash.cpython-310.pyc,,
|
||||||
|
nacl/bindings/__pycache__/crypto_hash.cpython-310.pyc,,
|
||||||
|
nacl/bindings/__pycache__/crypto_kx.cpython-310.pyc,,
|
||||||
|
nacl/bindings/__pycache__/crypto_pwhash.cpython-310.pyc,,
|
||||||
|
nacl/bindings/__pycache__/crypto_scalarmult.cpython-310.pyc,,
|
||||||
|
nacl/bindings/__pycache__/crypto_secretbox.cpython-310.pyc,,
|
||||||
|
nacl/bindings/__pycache__/crypto_secretstream.cpython-310.pyc,,
|
||||||
|
nacl/bindings/__pycache__/crypto_shorthash.cpython-310.pyc,,
|
||||||
|
nacl/bindings/__pycache__/crypto_sign.cpython-310.pyc,,
|
||||||
|
nacl/bindings/__pycache__/randombytes.cpython-310.pyc,,
|
||||||
|
nacl/bindings/__pycache__/sodium_core.cpython-310.pyc,,
|
||||||
|
nacl/bindings/__pycache__/utils.cpython-310.pyc,,
|
||||||
|
nacl/bindings/crypto_aead.py,sha256=BIw1k_JCfr5ylZk0RF5rCFIM1fhfLkEa-aiWkrfffNE,15597
|
||||||
|
nacl/bindings/crypto_box.py,sha256=Ox0NG2t4MsGhBAa7Kgah4o0gc99ULMsqkdX56ofOouY,10139
|
||||||
|
nacl/bindings/crypto_core.py,sha256=6u9G3y7H-QrawO785UkFFFtwDoCkeHE63GOUl9p5-eA,13736
|
||||||
|
nacl/bindings/crypto_generichash.py,sha256=9mX0DGIIzicr-uXrqFM1nU4tirasbixDwbcdfV7W1fc,8852
|
||||||
|
nacl/bindings/crypto_hash.py,sha256=Rg1rsEwE3azhsQT-dNVPA4NB9VogJAKn1EfxYt0pPe0,2175
|
||||||
|
nacl/bindings/crypto_kx.py,sha256=oZNVlNgROpHOa1XQ_uZe0tqIkdfuApeJlRnwR23_74k,6723
|
||||||
|
nacl/bindings/crypto_pwhash.py,sha256=laVDo4xFUuGyEjtZAU510AklBF6ablBy7Z3HN1WDYjY,18848
|
||||||
|
nacl/bindings/crypto_scalarmult.py,sha256=_DX-mst2uCnzjo6fP5HRTnhv1BC95B9gmJc3L_or16g,8244
|
||||||
|
nacl/bindings/crypto_secretbox.py,sha256=KgZ1VvkCJDlQ85jtfe9c02VofPvuEgZEhWni-aX3MsM,2914
|
||||||
|
nacl/bindings/crypto_secretstream.py,sha256=G0FgZS01qA5RzWzm5Bdms8Yy_lvgdZDoUYYBActPmvQ,11165
|
||||||
|
nacl/bindings/crypto_shorthash.py,sha256=PQU7djHTLDGdVs-w_TsivjFHHp5EK5k2Yh6p-6z0T60,2603
|
||||||
|
nacl/bindings/crypto_sign.py,sha256=53j2im9E4F79qT_2U8IfCAc3lzg0VMwEjvAPEUccVDg,10342
|
||||||
|
nacl/bindings/randombytes.py,sha256=uBK3W4WcjgnjZdWanrX0fjYZpr9KHbBgNMl9rui-Ojc,1563
|
||||||
|
nacl/bindings/sodium_core.py,sha256=9Y9CX--sq-TaPaQRPRpx8SWDSS9PJOja_Cqb-yqyJNQ,1039
|
||||||
|
nacl/bindings/utils.py,sha256=KDwQnadXeNMbqEA1SmpNyCVo5k8MiUQa07QM66VzfXM,4298
|
||||||
|
nacl/encoding.py,sha256=qTAPc2MXSkdh4cqDVY0ra6kHyViHMCmEo_re7cgGk5w,2915
|
||||||
|
nacl/exceptions.py,sha256=GZH32aJtZgqCO4uz0LRsev8z0WyvAYuV3YVqT9AAQq4,2451
|
||||||
|
nacl/hash.py,sha256=EYBOe6UVc9SUQINEmyuRSa1QGRSvdwdrBzTL1tdFLU8,6392
|
||||||
|
nacl/hashlib.py,sha256=L5Fv75St8AMPvb-GhA4YqX5p1mC_Sb4HhC1NxNQMpJA,4400
|
||||||
|
nacl/public.py,sha256=RVGCWQRjIJOmW-8sNrVLtsDjMMGx30i6UyfViGCnQNA,14792
|
||||||
|
nacl/pwhash/__init__.py,sha256=XSDXd7wQHNLEHl0mkHfVb5lFQsp6ygHkhen718h0BSM,2675
|
||||||
|
nacl/pwhash/__pycache__/__init__.cpython-310.pyc,,
|
||||||
|
nacl/pwhash/__pycache__/_argon2.cpython-310.pyc,,
|
||||||
|
nacl/pwhash/__pycache__/argon2i.cpython-310.pyc,,
|
||||||
|
nacl/pwhash/__pycache__/argon2id.cpython-310.pyc,,
|
||||||
|
nacl/pwhash/__pycache__/scrypt.cpython-310.pyc,,
|
||||||
|
nacl/pwhash/_argon2.py,sha256=jL1ChR9biwYh3RSuc-LJ2-W4DlVLHpir-XHGX8cpeJQ,1779
|
||||||
|
nacl/pwhash/argon2i.py,sha256=IIvIuO9siKUu5-Wpz0SGiltLQv7Du_mi9BUE8INRK_4,4405
|
||||||
|
nacl/pwhash/argon2id.py,sha256=H22i8O4j9Ws4L3JsXl9TRcJzDcyaVumhQRPzINAgJWM,4433
|
||||||
|
nacl/pwhash/scrypt.py,sha256=fMr3Qht1a1EY8aebNNntfLRjinIPXtKYKKrrBhY5LDc,6986
|
||||||
|
nacl/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||||
|
nacl/secret.py,sha256=kauBNuP-0rb3TjU2EMBMu5Vnmzjnscp1bRqMspy5LzU,12108
|
||||||
|
nacl/signing.py,sha256=kbTEUyHLUMaNLv1nCjxzGxCs82Qs5w8gxE_CnEwPuIU,8337
|
||||||
|
nacl/utils.py,sha256=gmlTD1x9ZNwzHd8LpALH1CHud-Htv8ejRb3y7TyS9f0,2341
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
Wheel-Version: 1.0
|
||||||
|
Generator: bdist_wheel (0.37.1)
|
||||||
|
Root-Is-Purelib: false
|
||||||
|
Tag: cp36-abi3-manylinux_2_17_x86_64
|
||||||
|
Tag: cp36-abi3-manylinux2014_x86_64
|
||||||
|
Tag: cp36-abi3-manylinux_2_24_x86_64
|
||||||
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
_sodium
|
||||||
|
nacl
|
||||||
BIN
Binary file not shown.
@@ -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
|
||||||
BIN
Binary file not shown.
BIN
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user