mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-10-26 17:21:58 -06:00
Better Organized Server.py into Commented Sections
This commit is contained in:
@@ -1,4 +1,22 @@
|
||||
#////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: <ProjectRoot>/Data/Server/server.py
|
||||
"""
|
||||
Borealis Server
|
||||
----------------
|
||||
Flask + Socket.IO application that fronts the Borealis platform. The module
|
||||
coordinates agent lifecycle, workflow storage, job scheduling, and supportive
|
||||
utilities exposed through HTTP and WebSocket interfaces.
|
||||
|
||||
Section Guide:
|
||||
* Logging & Repository Hash Tracking
|
||||
* Local Python Integrations
|
||||
* Runtime Stack Configuration
|
||||
* Authentication & Identity
|
||||
* Storage, Assemblies, and Legacy Script APIs
|
||||
* Agent Lifecycle and Scheduler Integration
|
||||
* Sites, Search, and Device Views
|
||||
* Quick Jobs, Ansible Reporting, and Service Accounts
|
||||
* WebSocket Event Handling and Entrypoint
|
||||
"""
|
||||
|
||||
import eventlet
|
||||
# Monkey-patch stdlib for cooperative sockets
|
||||
@@ -39,7 +57,10 @@ try:
|
||||
except Exception:
|
||||
qrcode = None # type: ignore
|
||||
|
||||
# Centralized logging (Server)
|
||||
# =============================================================================
|
||||
# Section: Logging & Observability
|
||||
# =============================================================================
|
||||
# Centralized writers for server/ansible logs with daily rotation under Logs/Server.
|
||||
def _server_logs_root() -> str:
|
||||
try:
|
||||
return os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'Logs', 'Server'))
|
||||
@@ -79,6 +100,10 @@ def _write_service_log(service: str, msg: str):
|
||||
pass
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Section: Repository Hash Tracking
|
||||
# =============================================================================
|
||||
# Cache GitHub repository heads so agents can poll without rate limit pressure.
|
||||
_REPO_HEAD_CACHE: Dict[str, Tuple[str, float]] = {}
|
||||
_REPO_HEAD_LOCK = Lock()
|
||||
|
||||
@@ -286,18 +311,23 @@ def _ensure_repo_hash_worker():
|
||||
def _ansible_log_server(msg: str):
|
||||
_write_service_log('ansible', msg)
|
||||
|
||||
# =============================================================================
|
||||
# Section: Local Python Integrations
|
||||
# =============================================================================
|
||||
# Shared constants and helper imports that bridge into bundled Python services.
|
||||
DEFAULT_SERVICE_ACCOUNT = '.\\svcBorealis'
|
||||
LEGACY_SERVICE_ACCOUNTS = {'.\\svcBorealisAnsibleRunner', 'svcBorealisAnsibleRunner'}
|
||||
|
||||
# Borealis Python API Endpoints
|
||||
from Python_API_Endpoints.ocr_engines import run_ocr_on_base64
|
||||
from Python_API_Endpoints.script_engines import run_powershell_script
|
||||
from job_scheduler import register as register_job_scheduler
|
||||
from job_scheduler import set_online_lookup as scheduler_set_online_lookup
|
||||
|
||||
# ---------------------------------------------
|
||||
# Flask + WebSocket Server Configuration
|
||||
# ---------------------------------------------
|
||||
# =============================================================================
|
||||
# Section: Runtime Stack Configuration
|
||||
# =============================================================================
|
||||
# Configure Flask, reverse-proxy awareness, CORS, and Socket.IO transport.
|
||||
|
||||
app = Flask(
|
||||
__name__,
|
||||
static_folder=os.path.join(os.path.dirname(__file__), '../web-interface/build'),
|
||||
@@ -344,9 +374,8 @@ socketio = SocketIO(
|
||||
_hydrate_repo_hash_cache_from_disk()
|
||||
_ensure_repo_hash_worker()
|
||||
|
||||
# ---------------------------------------------
|
||||
# Serve ReactJS Production Vite Build from dist/
|
||||
# ---------------------------------------------
|
||||
# Endpoint: / and /<path> — serve the built Vite SPA assets.
|
||||
|
||||
@app.route('/', defaults={'path': ''})
|
||||
@app.route('/<path:path>')
|
||||
def serve_dist(path):
|
||||
@@ -358,14 +387,14 @@ def serve_dist(path):
|
||||
return send_from_directory(app.static_folder, 'index.html')
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
# Health Check Endpoint
|
||||
# ---------------------------------------------
|
||||
# Endpoint: /health — liveness probe for orchestrators.
|
||||
|
||||
@app.route("/health")
|
||||
def health():
|
||||
return jsonify({"status": "ok"})
|
||||
|
||||
|
||||
# Endpoint: /api/repo/current_hash — cached GitHub head lookup for agents.
|
||||
@app.route("/api/repo/current_hash", methods=["GET"])
|
||||
def api_repo_current_hash():
|
||||
try:
|
||||
@@ -867,6 +896,8 @@ def _apply_agent_hash_update(agent_id: str, agent_hash: str, agent_guid: Optiona
|
||||
return payload, 200
|
||||
|
||||
|
||||
# Endpoint: /api/agent/hash — methods GET, POST.
|
||||
|
||||
@app.route("/api/agent/hash", methods=["GET", "POST"])
|
||||
def api_agent_hash():
|
||||
if request.method == 'GET':
|
||||
@@ -897,6 +928,8 @@ def api_agent_hash():
|
||||
return jsonify(payload), status
|
||||
|
||||
|
||||
# Endpoint: /api/agent/hash_list — methods GET.
|
||||
|
||||
@app.route("/api/agent/hash_list", methods=["GET"])
|
||||
def api_agent_hash_list():
|
||||
try:
|
||||
@@ -907,9 +940,8 @@ def api_agent_hash_list():
|
||||
return jsonify({'error': 'internal error'}), 500
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
# Server Time Endpoint
|
||||
# ---------------------------------------------
|
||||
# Endpoint: /api/server/time — return current UTC timestamp metadata.
|
||||
|
||||
@app.route("/api/server/time", methods=["GET"])
|
||||
def api_server_time():
|
||||
try:
|
||||
@@ -944,9 +976,11 @@ def api_server_time():
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
# ---------------------------------------------
|
||||
# Auth + Users (DB-backed)
|
||||
# ---------------------------------------------
|
||||
# =============================================================================
|
||||
# Section: Authentication & User Accounts
|
||||
# =============================================================================
|
||||
# Credential storage, MFA helpers, and user management endpoints.
|
||||
|
||||
def _now_ts() -> int:
|
||||
return int(time.time())
|
||||
|
||||
@@ -1108,9 +1142,11 @@ def _require_admin():
|
||||
return None
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
# Token helpers (for dev/proxy-friendly auth)
|
||||
# ---------------------------------------------
|
||||
# =============================================================================
|
||||
# Section: Token & Session Utilities
|
||||
# =============================================================================
|
||||
# URL-safe serializers that back login cookies and recovery flows.
|
||||
|
||||
def _token_serializer():
|
||||
secret = app.secret_key or 'borealis-dev-secret'
|
||||
return URLSafeTimedSerializer(secret, salt='borealis-auth')
|
||||
@@ -1132,6 +1168,8 @@ def _verify_token(token: str):
|
||||
return None
|
||||
|
||||
|
||||
# Endpoint: /api/auth/login — methods POST.
|
||||
|
||||
@app.route("/api/auth/login", methods=["POST"])
|
||||
def api_login():
|
||||
payload = request.get_json(silent=True) or {}
|
||||
@@ -1227,6 +1265,8 @@ def api_login():
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
# Endpoint: /api/auth/logout — methods POST.
|
||||
|
||||
@app.route("/api/auth/logout", methods=["POST"]) # simple logout
|
||||
def api_logout():
|
||||
session.clear()
|
||||
@@ -1236,6 +1276,8 @@ def api_logout():
|
||||
return resp
|
||||
|
||||
|
||||
# Endpoint: /api/auth/mfa/verify — methods POST.
|
||||
|
||||
@app.route("/api/auth/mfa/verify", methods=["POST"])
|
||||
def api_mfa_verify():
|
||||
pending = session.get("mfa_pending") or {}
|
||||
@@ -1291,6 +1333,8 @@ def api_mfa_verify():
|
||||
return jsonify({"error": str(exc)}), 500
|
||||
|
||||
|
||||
# Endpoint: /api/auth/me — methods GET.
|
||||
|
||||
@app.route("/api/auth/me", methods=["GET"]) # whoami
|
||||
def api_me():
|
||||
user = _current_user()
|
||||
@@ -1325,6 +1369,8 @@ def api_me():
|
||||
})
|
||||
|
||||
|
||||
# Endpoint: /api/users — methods GET.
|
||||
|
||||
@app.route("/api/users", methods=["GET"])
|
||||
def api_users_list():
|
||||
chk = _require_admin()
|
||||
@@ -1356,6 +1402,8 @@ def api_users_list():
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
# Endpoint: /api/users — methods POST.
|
||||
|
||||
@app.route("/api/users", methods=["POST"]) # create user
|
||||
def api_users_create():
|
||||
chk = _require_admin()
|
||||
@@ -1387,6 +1435,8 @@ def api_users_create():
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
# Endpoint: /api/users/<username> — methods DELETE.
|
||||
|
||||
@app.route("/api/users/<username>", methods=["DELETE"]) # delete user
|
||||
def api_users_delete(username):
|
||||
chk = _require_admin()
|
||||
@@ -1420,6 +1470,8 @@ def api_users_delete(username):
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
# Endpoint: /api/users/<username>/reset_password — methods POST.
|
||||
|
||||
@app.route("/api/users/<username>/reset_password", methods=["POST"]) # reset password
|
||||
def api_users_reset_password(username):
|
||||
chk = _require_admin()
|
||||
@@ -1447,6 +1499,8 @@ def api_users_reset_password(username):
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
# Endpoint: /api/users/<username>/role — methods POST.
|
||||
|
||||
@app.route("/api/users/<username>/role", methods=["POST"]) # change role
|
||||
def api_users_change_role(username):
|
||||
chk = _require_admin()
|
||||
@@ -1487,6 +1541,8 @@ def api_users_change_role(username):
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
# Endpoint: /api/users/<username>/mfa — methods POST.
|
||||
|
||||
@app.route("/api/users/<username>/mfa", methods=["POST"])
|
||||
def api_users_toggle_mfa(username):
|
||||
chk = _require_admin()
|
||||
@@ -1537,11 +1593,13 @@ def api_users_toggle_mfa(username):
|
||||
except Exception as exc:
|
||||
return jsonify({"error": str(exc)}), 500
|
||||
|
||||
# ---------------------------------------------
|
||||
# Borealis Python API Endpoints
|
||||
# ---------------------------------------------
|
||||
# /api/ocr: Accepts a base64 image and OCR engine selection,
|
||||
# and returns extracted text lines.
|
||||
# =============================================================================
|
||||
# Section: Python Sidecar Services
|
||||
# =============================================================================
|
||||
# Bridge into Python helpers such as OCR and PowerShell execution.
|
||||
|
||||
# Endpoint: /api/ocr — methods POST.
|
||||
|
||||
@app.route("/api/ocr", methods=["POST"])
|
||||
def ocr_endpoint():
|
||||
payload = request.get_json()
|
||||
@@ -1564,9 +1622,11 @@ def ocr_endpoint():
|
||||
|
||||
# unified assembly endpoints supersede prior storage workflow endpoints
|
||||
|
||||
# ---------------------------------------------
|
||||
# Borealis Storage API Endpoints
|
||||
# ---------------------------------------------
|
||||
# =============================================================================
|
||||
# Section: Storage Legacy Helpers
|
||||
# =============================================================================
|
||||
# Compatibility helpers for direct script/storage file access.
|
||||
|
||||
def _safe_read_json(path: str) -> Dict:
|
||||
"""
|
||||
Try to read JSON safely. Returns {} on failure.
|
||||
@@ -1602,9 +1662,11 @@ def _extract_tab_name(obj: Dict) -> str:
|
||||
# superseded by /api/assembly/rename
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
# Unified Assembly API (Workflows, Scripts, Playbooks)
|
||||
# ---------------------------------------------
|
||||
# =============================================================================
|
||||
# Section: Assemblies CRUD
|
||||
# =============================================================================
|
||||
# Unified workflow/script/playbook storage with path normalization.
|
||||
|
||||
|
||||
def _assemblies_root() -> str:
|
||||
return os.path.abspath(
|
||||
@@ -1876,6 +1938,8 @@ def _load_assembly_document(abs_path: str, island: str, type_hint: str = "") ->
|
||||
return doc
|
||||
|
||||
|
||||
# Endpoint: /api/assembly/create — methods POST.
|
||||
|
||||
@app.route("/api/assembly/create", methods=["POST"])
|
||||
def assembly_create():
|
||||
data = request.get_json(silent=True) or {}
|
||||
@@ -1939,6 +2003,8 @@ def assembly_create():
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
# Endpoint: /api/assembly/edit — methods POST.
|
||||
|
||||
@app.route("/api/assembly/edit", methods=["POST"])
|
||||
def assembly_edit():
|
||||
data = request.get_json(silent=True) or {}
|
||||
@@ -1991,6 +2057,8 @@ def assembly_edit():
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
# Endpoint: /api/assembly/rename — methods POST.
|
||||
|
||||
@app.route("/api/assembly/rename", methods=["POST"])
|
||||
def assembly_rename():
|
||||
data = request.get_json(silent=True) or {}
|
||||
@@ -2045,6 +2113,8 @@ def assembly_rename():
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
# Endpoint: /api/assembly/move — methods POST.
|
||||
|
||||
@app.route("/api/assembly/move", methods=["POST"])
|
||||
def assembly_move():
|
||||
data = request.get_json(silent=True) or {}
|
||||
@@ -2070,6 +2140,8 @@ def assembly_move():
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
# Endpoint: /api/assembly/delete — methods POST.
|
||||
|
||||
@app.route("/api/assembly/delete", methods=["POST"])
|
||||
def assembly_delete():
|
||||
data = request.get_json(silent=True) or {}
|
||||
@@ -2097,6 +2169,8 @@ def assembly_delete():
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
# Endpoint: /api/assembly/list — methods GET.
|
||||
|
||||
@app.route("/api/assembly/list", methods=["GET"])
|
||||
def assembly_list():
|
||||
"""List files and folders for a given island (workflows|scripts|ansible)."""
|
||||
@@ -2199,6 +2273,8 @@ def assembly_list():
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
# Endpoint: /api/assembly/load — methods GET.
|
||||
|
||||
@app.route("/api/assembly/load", methods=["GET"])
|
||||
def assembly_load():
|
||||
"""Load a file for a given island. Returns workflow JSON for workflows, and text content for others."""
|
||||
@@ -2229,9 +2305,11 @@ def assembly_load():
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
# Scripts Storage API Endpoints
|
||||
# ---------------------------------------------
|
||||
# =============================================================================
|
||||
# Section: Scripts File API (Deprecated)
|
||||
# =============================================================================
|
||||
# Older per-file script endpoints retained for backward compatibility.
|
||||
|
||||
def _scripts_root() -> str:
|
||||
# Scripts live under Assemblies. We unify listing under Assemblies and
|
||||
# only allow access within top-level folders: "Scripts" and "Ansible Playbooks".
|
||||
@@ -2287,6 +2365,8 @@ def _ext_for_type(script_type: str) -> str:
|
||||
"""
|
||||
Legacy scripts endpoints removed in favor of unified assembly APIs.
|
||||
"""
|
||||
# Endpoint: /api/scripts/list — methods GET.
|
||||
|
||||
@app.route("/api/scripts/list", methods=["GET"])
|
||||
def list_scripts():
|
||||
"""Scan <ProjectRoot>/Assemblies/Scripts for script files and return list + folders."""
|
||||
@@ -2341,6 +2421,8 @@ def list_scripts():
|
||||
return jsonify({"error": "deprecated; use /api/assembly/list?island=scripts"}), 410
|
||||
|
||||
|
||||
# Endpoint: /api/scripts/load — methods GET.
|
||||
|
||||
@app.route("/api/scripts/load", methods=["GET"])
|
||||
def load_script():
|
||||
rel_path = request.args.get("path", "")
|
||||
@@ -2356,6 +2438,8 @@ def load_script():
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
# Endpoint: /api/scripts/save — methods POST.
|
||||
|
||||
@app.route("/api/scripts/save", methods=["POST"])
|
||||
def save_script():
|
||||
data = request.get_json(silent=True) or {}
|
||||
@@ -2401,6 +2485,8 @@ def save_script():
|
||||
return jsonify({"error": "deprecated; use /api/assembly/create or /api/assembly/edit"}), 410
|
||||
|
||||
|
||||
# Endpoint: /api/scripts/rename_file — methods POST.
|
||||
|
||||
@app.route("/api/scripts/rename_file", methods=["POST"])
|
||||
def rename_script_file():
|
||||
data = request.get_json(silent=True) or {}
|
||||
@@ -2422,6 +2508,8 @@ def rename_script_file():
|
||||
return jsonify({"error": "deprecated; use /api/assembly/rename"}), 410
|
||||
|
||||
|
||||
# Endpoint: /api/scripts/move_file — methods POST.
|
||||
|
||||
@app.route("/api/scripts/move_file", methods=["POST"])
|
||||
def move_script_file():
|
||||
data = request.get_json(silent=True) or {}
|
||||
@@ -2438,6 +2526,8 @@ def move_script_file():
|
||||
return jsonify({"error": "deprecated; use /api/assembly/move"}), 410
|
||||
|
||||
|
||||
# Endpoint: /api/scripts/delete_file — methods POST.
|
||||
|
||||
@app.route("/api/scripts/delete_file", methods=["POST"])
|
||||
def delete_script_file():
|
||||
data = request.get_json(silent=True) or {}
|
||||
@@ -2448,9 +2538,11 @@ def delete_script_file():
|
||||
return jsonify({"error": "File not found"}), 404
|
||||
return jsonify({"error": "deprecated; use /api/assembly/delete"}), 410
|
||||
|
||||
# ---------------------------------------------
|
||||
# Ansible Playbooks Storage API Endpoints
|
||||
# ---------------------------------------------
|
||||
# =============================================================================
|
||||
# Section: Ansible File API (Deprecated)
|
||||
# =============================================================================
|
||||
# Legacy Ansible playbook file operations pending assembly migration.
|
||||
|
||||
def _ansible_root() -> str:
|
||||
return os.path.abspath(
|
||||
os.path.join(os.path.dirname(__file__), "..", "..", "Assemblies", "Ansible_Playbooks")
|
||||
@@ -2466,6 +2558,8 @@ def _is_valid_ansible_relpath(rel_path: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
# Endpoint: /api/ansible/list — methods GET.
|
||||
|
||||
@app.route("/api/ansible/list", methods=["GET"])
|
||||
def list_ansible():
|
||||
"""Scan <ProjectRoot>/Assemblies/Ansible_Playbooks for .yml playbooks and return list + folders."""
|
||||
@@ -2498,6 +2592,8 @@ def list_ansible():
|
||||
return jsonify({"error": "deprecated; use /api/assembly/list?island=ansible"}), 410
|
||||
|
||||
|
||||
# Endpoint: /api/ansible/load — methods GET.
|
||||
|
||||
@app.route("/api/ansible/load", methods=["GET"])
|
||||
def load_ansible():
|
||||
rel_path = request.args.get("path", "")
|
||||
@@ -2513,6 +2609,8 @@ def load_ansible():
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
# Endpoint: /api/ansible/save — methods POST.
|
||||
|
||||
@app.route("/api/ansible/save", methods=["POST"])
|
||||
def save_ansible():
|
||||
data = request.get_json(silent=True) or {}
|
||||
@@ -2541,6 +2639,8 @@ def save_ansible():
|
||||
return jsonify({"error": "deprecated; use /api/assembly/create or /api/assembly/edit"}), 410
|
||||
|
||||
|
||||
# Endpoint: /api/ansible/rename_file — methods POST.
|
||||
|
||||
@app.route("/api/ansible/rename_file", methods=["POST"])
|
||||
def rename_ansible_file():
|
||||
data = request.get_json(silent=True) or {}
|
||||
@@ -2558,6 +2658,8 @@ def rename_ansible_file():
|
||||
return jsonify({"error": "deprecated; use /api/assembly/rename"}), 410
|
||||
|
||||
|
||||
# Endpoint: /api/ansible/move_file — methods POST.
|
||||
|
||||
@app.route("/api/ansible/move_file", methods=["POST"])
|
||||
def move_ansible_file():
|
||||
data = request.get_json(silent=True) or {}
|
||||
@@ -2574,6 +2676,8 @@ def move_ansible_file():
|
||||
return jsonify({"error": "deprecated; use /api/assembly/move"}), 410
|
||||
|
||||
|
||||
# Endpoint: /api/ansible/delete_file — methods POST.
|
||||
|
||||
@app.route("/api/ansible/delete_file", methods=["POST"])
|
||||
def delete_ansible_file():
|
||||
data = request.get_json(silent=True) or {}
|
||||
@@ -2585,6 +2689,8 @@ def delete_ansible_file():
|
||||
return jsonify({"error": "deprecated; use /api/assembly/delete"}), 410
|
||||
|
||||
|
||||
# Endpoint: /api/ansible/create_folder — methods POST.
|
||||
|
||||
@app.route("/api/ansible/create_folder", methods=["POST"])
|
||||
def ansible_create_folder():
|
||||
data = request.get_json(silent=True) or {}
|
||||
@@ -2597,6 +2703,8 @@ def ansible_create_folder():
|
||||
return jsonify({"error": "deprecated; use /api/assembly/create"}), 410
|
||||
|
||||
|
||||
# Endpoint: /api/ansible/delete_folder — methods POST.
|
||||
|
||||
@app.route("/api/ansible/delete_folder", methods=["POST"])
|
||||
def ansible_delete_folder():
|
||||
data = request.get_json(silent=True) or {}
|
||||
@@ -2611,6 +2719,8 @@ def ansible_delete_folder():
|
||||
return jsonify({"error": "deprecated; use /api/assembly/delete"}), 410
|
||||
|
||||
|
||||
# Endpoint: /api/ansible/rename_folder — methods POST.
|
||||
|
||||
@app.route("/api/ansible/rename_folder", methods=["POST"])
|
||||
def ansible_rename_folder():
|
||||
data = request.get_json(silent=True) or {}
|
||||
@@ -2629,6 +2739,8 @@ def ansible_rename_folder():
|
||||
return jsonify({"error": "deprecated; use /api/assembly/rename"}), 410
|
||||
|
||||
|
||||
# Endpoint: /api/scripts/create_folder — methods POST.
|
||||
|
||||
@app.route("/api/scripts/create_folder", methods=["POST"])
|
||||
def scripts_create_folder():
|
||||
data = request.get_json(silent=True) or {}
|
||||
@@ -2644,6 +2756,8 @@ def scripts_create_folder():
|
||||
return jsonify({"error": "deprecated; use /api/assembly/create"}), 410
|
||||
|
||||
|
||||
# Endpoint: /api/scripts/delete_folder — methods POST.
|
||||
|
||||
@app.route("/api/scripts/delete_folder", methods=["POST"])
|
||||
def scripts_delete_folder():
|
||||
data = request.get_json(silent=True) or {}
|
||||
@@ -2658,6 +2772,8 @@ def scripts_delete_folder():
|
||||
return jsonify({"error": "deprecated; use /api/assembly/delete"}), 410
|
||||
|
||||
|
||||
# Endpoint: /api/scripts/rename_folder — methods POST.
|
||||
|
||||
@app.route("/api/scripts/rename_folder", methods=["POST"])
|
||||
def scripts_rename_folder():
|
||||
data = request.get_json(silent=True) or {}
|
||||
@@ -2675,12 +2791,11 @@ def scripts_rename_folder():
|
||||
new_abs = os.path.join(os.path.dirname(old_abs), new_name)
|
||||
return jsonify({"error": "deprecated; use /api/assembly/rename"}), 410
|
||||
|
||||
# ---------------------------------------------
|
||||
# Borealis Agent API Endpoints
|
||||
# ---------------------------------------------
|
||||
# These endpoints handle agent registration, provisioning, image streaming, and heartbeats.
|
||||
# Shape expected by UI for each agent:
|
||||
# { "agent_id", "hostname", "agent_operating_system", "last_seen", "status" }
|
||||
# =============================================================================
|
||||
# Section: Agent Lifecycle API
|
||||
# =============================================================================
|
||||
# Agent registration, configuration, device persistence, and screenshot streaming metadata.
|
||||
|
||||
registered_agents: Dict[str, Dict] = {}
|
||||
agent_configurations: Dict[str, Dict] = {}
|
||||
latest_images: Dict[str, Dict] = {}
|
||||
@@ -3501,9 +3616,11 @@ def ensure_default_admin():
|
||||
|
||||
ensure_default_admin()
|
||||
|
||||
# ---------------------------------------------
|
||||
# Scheduler Registration
|
||||
# ---------------------------------------------
|
||||
# =============================================================================
|
||||
# Section: Scheduler Integration
|
||||
# =============================================================================
|
||||
# Connect the Flask app to the background job scheduler and helpers.
|
||||
|
||||
job_scheduler = register_job_scheduler(app, socketio, DB_PATH)
|
||||
job_scheduler.start()
|
||||
|
||||
@@ -3524,9 +3641,11 @@ def _online_hostnames_snapshot():
|
||||
|
||||
scheduler_set_online_lookup(job_scheduler, _online_hostnames_snapshot)
|
||||
|
||||
# ---------------------------------------------
|
||||
# Sites API
|
||||
# ---------------------------------------------
|
||||
# =============================================================================
|
||||
# Section: Site Management API
|
||||
# =============================================================================
|
||||
# CRUD for site records and device membership.
|
||||
|
||||
|
||||
def _row_to_site(row):
|
||||
# id, name, description, created_at, device_count
|
||||
@@ -3539,6 +3658,8 @@ def _row_to_site(row):
|
||||
}
|
||||
|
||||
|
||||
# Endpoint: /api/sites — methods GET.
|
||||
|
||||
@app.route("/api/sites", methods=["GET"])
|
||||
def list_sites():
|
||||
try:
|
||||
@@ -3564,6 +3685,8 @@ def list_sites():
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
# Endpoint: /api/sites — methods POST.
|
||||
|
||||
@app.route("/api/sites", methods=["POST"])
|
||||
def create_site():
|
||||
payload = request.get_json(silent=True) or {}
|
||||
@@ -3595,6 +3718,8 @@ def create_site():
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
# Endpoint: /api/sites/delete — methods POST.
|
||||
|
||||
@app.route("/api/sites/delete", methods=["POST"])
|
||||
def delete_sites():
|
||||
payload = request.get_json(silent=True) or {}
|
||||
@@ -3630,6 +3755,8 @@ def delete_sites():
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
# Endpoint: /api/sites/device_map — methods GET.
|
||||
|
||||
@app.route("/api/sites/device_map", methods=["GET"])
|
||||
def sites_device_map():
|
||||
"""
|
||||
@@ -3675,6 +3802,8 @@ def sites_device_map():
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
# Endpoint: /api/sites/assign — methods POST.
|
||||
|
||||
@app.route("/api/sites/assign", methods=["POST"])
|
||||
def assign_devices_to_site():
|
||||
payload = request.get_json(silent=True) or {}
|
||||
@@ -3713,6 +3842,8 @@ def assign_devices_to_site():
|
||||
|
||||
|
||||
# Rename a site (update name only)
|
||||
# Endpoint: /api/sites/rename — methods POST.
|
||||
|
||||
@app.route("/api/sites/rename", methods=["POST"])
|
||||
def rename_site():
|
||||
payload = request.get_json(silent=True) or {}
|
||||
@@ -3755,9 +3886,11 @@ def rename_site():
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
# Global Search (suggestions)
|
||||
# ---------------------------------------------
|
||||
# =============================================================================
|
||||
# Section: Global Search API
|
||||
# =============================================================================
|
||||
# Lightweight search surface for auto-complete over device data.
|
||||
|
||||
|
||||
def _load_device_records(limit: int = 0):
|
||||
"""
|
||||
@@ -3809,6 +3942,8 @@ def _load_device_records(limit: int = 0):
|
||||
return out
|
||||
|
||||
|
||||
# Endpoint: /api/search/suggest — methods GET.
|
||||
|
||||
@app.route("/api/search/suggest", methods=["GET"])
|
||||
def search_suggest():
|
||||
"""
|
||||
@@ -3899,6 +4034,8 @@ def search_suggest():
|
||||
})
|
||||
|
||||
|
||||
# Endpoint: /api/devices — methods GET.
|
||||
|
||||
@app.route("/api/devices", methods=["GET"])
|
||||
def list_devices():
|
||||
"""Return all devices with expanded columns for the WebUI."""
|
||||
@@ -3974,6 +4111,8 @@ def list_devices():
|
||||
return jsonify({"devices": devices})
|
||||
|
||||
|
||||
# Endpoint: /api/devices/<guid> — methods GET.
|
||||
|
||||
@app.route("/api/devices/<guid>", methods=["GET"])
|
||||
def get_device_by_guid(guid: str):
|
||||
try:
|
||||
@@ -4022,9 +4161,11 @@ def get_device_by_guid(guid: str):
|
||||
return jsonify(payload)
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
# Device List Views API
|
||||
# ---------------------------------------------
|
||||
# =============================================================================
|
||||
# Section: Device List Views
|
||||
# =============================================================================
|
||||
# Persisted filters/layouts for the device list interface.
|
||||
|
||||
def _row_to_view(row):
|
||||
return {
|
||||
"id": row[0],
|
||||
@@ -4036,6 +4177,8 @@ def _row_to_view(row):
|
||||
}
|
||||
|
||||
|
||||
# Endpoint: /api/device_list_views — methods GET.
|
||||
|
||||
@app.route("/api/device_list_views", methods=["GET"])
|
||||
def list_device_list_views():
|
||||
try:
|
||||
@@ -4051,6 +4194,8 @@ def list_device_list_views():
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
# Endpoint: /api/device_list_views/<int:view_id> — methods GET.
|
||||
|
||||
@app.route("/api/device_list_views/<int:view_id>", methods=["GET"])
|
||||
def get_device_list_view(view_id: int):
|
||||
try:
|
||||
@@ -4069,6 +4214,8 @@ def get_device_list_view(view_id: int):
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
# Endpoint: /api/device_list_views — methods POST.
|
||||
|
||||
@app.route("/api/device_list_views", methods=["POST"])
|
||||
def create_device_list_view():
|
||||
payload = request.get_json(silent=True) or {}
|
||||
@@ -4108,6 +4255,8 @@ def create_device_list_view():
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
# Endpoint: /api/device_list_views/<int:view_id> — methods PUT.
|
||||
|
||||
@app.route("/api/device_list_views/<int:view_id>", methods=["PUT"])
|
||||
def update_device_list_view(view_id: int):
|
||||
payload = request.get_json(silent=True) or {}
|
||||
@@ -4162,6 +4311,8 @@ def update_device_list_view(view_id: int):
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
# Endpoint: /api/device_list_views/<int:view_id> — methods DELETE.
|
||||
|
||||
@app.route("/api/device_list_views/<int:view_id>", methods=["DELETE"])
|
||||
def delete_device_list_view(view_id: int):
|
||||
try:
|
||||
@@ -4470,6 +4621,8 @@ def _ensure_agent_guid(agent_id: str, hostname: Optional[str] = None) -> Optiona
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Endpoint: /api/agents — methods GET.
|
||||
|
||||
@app.route("/api/agents")
|
||||
def get_agents():
|
||||
"""Return agents with collector activity indicator."""
|
||||
@@ -4522,6 +4675,8 @@ def _deep_merge_preserve(prev: dict, incoming: dict) -> dict:
|
||||
return out
|
||||
|
||||
|
||||
# Endpoint: /api/agent/details — methods POST.
|
||||
|
||||
@app.route("/api/agent/details", methods=["POST"])
|
||||
def save_agent_details():
|
||||
data = request.get_json(silent=True) or {}
|
||||
@@ -4662,6 +4817,8 @@ def save_agent_details():
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
# Endpoint: /api/device/details/<hostname> — methods GET.
|
||||
|
||||
@app.route("/api/device/details/<hostname>", methods=["GET"])
|
||||
def get_device_details(hostname: str):
|
||||
try:
|
||||
@@ -4703,6 +4860,8 @@ def get_device_details(hostname: str):
|
||||
return jsonify({})
|
||||
|
||||
|
||||
# Endpoint: /api/device/description/<hostname> — methods POST.
|
||||
|
||||
@app.route("/api/device/description/<hostname>", methods=["POST"])
|
||||
def set_device_description(hostname: str):
|
||||
data = request.get_json(silent=True) or {}
|
||||
@@ -4741,9 +4900,11 @@ def set_device_description(hostname: str):
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
# Quick Job Execution + Activity History
|
||||
# ---------------------------------------------
|
||||
# =============================================================================
|
||||
# Section: Quick Jobs & Activity
|
||||
# =============================================================================
|
||||
# Submit ad-hoc runs and expose execution history for devices.
|
||||
|
||||
def _detect_script_type(fn: str) -> str:
|
||||
fn_lower = (fn or "").lower()
|
||||
if fn_lower.endswith(".json") and os.path.isfile(fn):
|
||||
@@ -4912,6 +5073,8 @@ def _rewrite_powershell_script(content: str, literal_lookup: Dict[str, str]) ->
|
||||
return _ENV_VAR_PATTERN.sub(_replace, content)
|
||||
|
||||
|
||||
# Endpoint: /api/scripts/quick_run — methods POST.
|
||||
|
||||
@app.route("/api/scripts/quick_run", methods=["POST"])
|
||||
def scripts_quick_run():
|
||||
"""Queue a Quick Job to agents via WebSocket and record Running status.
|
||||
@@ -5021,6 +5184,8 @@ def scripts_quick_run():
|
||||
return jsonify({"results": results})
|
||||
|
||||
|
||||
# Endpoint: /api/ansible/quick_run — methods POST.
|
||||
|
||||
@app.route("/api/ansible/quick_run", methods=["POST"])
|
||||
def ansible_quick_run():
|
||||
"""Queue an Ansible Playbook Quick Job via WebSocket to targeted agents.
|
||||
@@ -5121,6 +5286,8 @@ def ansible_quick_run():
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
# Endpoint: /api/device/activity/<hostname> — methods GET, DELETE.
|
||||
|
||||
@app.route("/api/device/activity/<hostname>", methods=["GET", "DELETE"])
|
||||
def device_activity(hostname: str):
|
||||
try:
|
||||
@@ -5155,6 +5322,8 @@ def device_activity(hostname: str):
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
# Endpoint: /api/device/activity/job/<int:job_id> — methods GET.
|
||||
|
||||
@app.route("/api/device/activity/job/<int:job_id>", methods=["GET"])
|
||||
def device_activity_job(job_id: int):
|
||||
try:
|
||||
@@ -5251,9 +5420,11 @@ def handle_quick_job_result(data):
|
||||
pass
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
# Ansible Runtime API (Play Recaps)
|
||||
# ---------------------------------------------
|
||||
# =============================================================================
|
||||
# Section: Ansible Runtime Reporting
|
||||
# =============================================================================
|
||||
# Collect and return Ansible recap payloads emitted by agents.
|
||||
|
||||
def _json_dump_safe(obj) -> str:
|
||||
try:
|
||||
if isinstance(obj, str):
|
||||
@@ -5265,9 +5436,11 @@ def _json_dump_safe(obj) -> str:
|
||||
return json.dumps({})
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
# Agent Service Account (WinRM localhost) APIs
|
||||
# ---------------------------------------------
|
||||
# =============================================================================
|
||||
# Section: Service Account Rotation
|
||||
# =============================================================================
|
||||
# Manage local service account secrets for SYSTEM PowerShell runs.
|
||||
|
||||
def _now_iso_utc() -> str:
|
||||
try:
|
||||
return datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z')
|
||||
@@ -5329,6 +5502,8 @@ def _service_acct_set(conn, agent_id: str, username: str, plaintext_password: st
|
||||
|
||||
|
||||
|
||||
# Endpoint: /api/agent/checkin — methods POST.
|
||||
|
||||
@app.route('/api/agent/checkin', methods=['POST'])
|
||||
def api_agent_checkin():
|
||||
payload = request.get_json(silent=True) or {}
|
||||
@@ -5394,6 +5569,8 @@ def api_agent_checkin():
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
# Endpoint: /api/agent/service-account/rotate — methods POST.
|
||||
|
||||
@app.route('/api/agent/service-account/rotate', methods=['POST'])
|
||||
def api_agent_service_account_rotate():
|
||||
payload = request.get_json(silent=True) or {}
|
||||
@@ -5424,6 +5601,8 @@ def api_agent_service_account_rotate():
|
||||
_ansible_log_server(f"[rotate] error agent_id={agent_id} err={e}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
# Endpoint: /api/ansible/recap/report — methods POST.
|
||||
|
||||
@app.route("/api/ansible/recap/report", methods=["POST"])
|
||||
def api_ansible_recap_report():
|
||||
"""Create or update an Ansible recap row for a running/finished playbook.
|
||||
@@ -5654,6 +5833,8 @@ def api_ansible_recap_report():
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
# Endpoint: /api/ansible/recaps — methods GET.
|
||||
|
||||
@app.route("/api/ansible/recaps", methods=["GET"])
|
||||
def api_ansible_recaps_list():
|
||||
"""List Ansible play recaps. Optional query params: hostname, limit (default 50)"""
|
||||
@@ -5706,6 +5887,8 @@ def api_ansible_recaps_list():
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
# Endpoint: /api/ansible/recap/<int:recap_id> — methods GET.
|
||||
|
||||
@app.route("/api/ansible/recap/<int:recap_id>", methods=["GET"])
|
||||
def api_ansible_recap_get(recap_id: int):
|
||||
try:
|
||||
@@ -5741,6 +5924,8 @@ def api_ansible_recap_get(recap_id: int):
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
# Endpoint: /api/ansible/run_for_activity/<int:activity_id> — methods GET.
|
||||
|
||||
@app.route("/api/ansible/run_for_activity/<int:activity_id>", methods=["GET"])
|
||||
def api_ansible_run_for_activity(activity_id: int):
|
||||
"""Return the latest run_id/status for a recap row linked to an activity_history id."""
|
||||
@@ -5860,6 +6045,8 @@ def handle_collector_status(data):
|
||||
pass
|
||||
|
||||
|
||||
# Endpoint: /api/agent/<agent_id> — methods DELETE.
|
||||
|
||||
@app.route("/api/agent/<agent_id>", methods=["DELETE"])
|
||||
def delete_agent(agent_id: str):
|
||||
"""Remove an agent from the registry and database."""
|
||||
@@ -5873,6 +6060,8 @@ def delete_agent(agent_id: str):
|
||||
return jsonify({"status": "removed"})
|
||||
return jsonify({"error": "agent not found"}), 404
|
||||
|
||||
# Endpoint: /api/agent/provision — methods POST.
|
||||
|
||||
@app.route("/api/agent/provision", methods=["POST"])
|
||||
def provision_agent():
|
||||
data = request.json
|
||||
@@ -5896,9 +6085,8 @@ def provision_agent():
|
||||
socketio.emit("agent_config", {**config, "agent_id": agent_id}, to=agent_id)
|
||||
return jsonify({"status": "provisioned", "roles": roles})
|
||||
|
||||
# ---------------------------------------------
|
||||
# Borealis External API Proxy Endpoint
|
||||
# ---------------------------------------------
|
||||
# Endpoint: /api/proxy — fan-out HTTP proxy used by agents.
|
||||
|
||||
@app.route("/api/proxy", methods=["GET", "POST", "OPTIONS"])
|
||||
def proxy():
|
||||
target = request.args.get("url")
|
||||
@@ -5921,9 +6109,8 @@ def proxy():
|
||||
resp.headers[k] = v
|
||||
return resp
|
||||
|
||||
# ---------------------------------------------
|
||||
# Live Screenshot Viewer for Debugging
|
||||
# ---------------------------------------------
|
||||
# Endpoint: /api/agent/<agent_id>/node/<node_id>/screenshot/live — lightweight diagnostic viewer.
|
||||
|
||||
@app.route("/api/agent/<agent_id>/node/<node_id>/screenshot/live")
|
||||
def screenshot_node_viewer(agent_id, node_id):
|
||||
return f"""
|
||||
@@ -5979,9 +6166,11 @@ def screenshot_node_viewer(agent_id, node_id):
|
||||
</html>
|
||||
"""
|
||||
|
||||
# ---------------------------------------------
|
||||
# WebSocket Events for Real-Time Communication
|
||||
# ---------------------------------------------
|
||||
# =============================================================================
|
||||
# Section: WebSocket Event Handlers
|
||||
# =============================================================================
|
||||
# Realtime channels for screenshots, macros, windows, and Ansible control.
|
||||
|
||||
@socketio.on("agent_screenshot_task")
|
||||
def receive_screenshot_task(data):
|
||||
agent_id = data.get("agent_id")
|
||||
@@ -6166,9 +6355,11 @@ def relay_ansible_run(data):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ---------------------------------------------
|
||||
# Server Launch
|
||||
# ---------------------------------------------
|
||||
# =============================================================================
|
||||
# Section: Module Entrypoint
|
||||
# =============================================================================
|
||||
# Run the Socket.IO-enabled Flask server when executed as __main__.
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Use SocketIO runner so WebSocket transport works with eventlet.
|
||||
socketio.run(app, host="0.0.0.0", port=5000)
|
||||
|
||||
Reference in New Issue
Block a user