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//") 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/") 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)