Better Organized Server.py into Commented Sections

This commit is contained in:
2025-10-10 13:09:43 -06:00
parent 6e52b42859
commit d238ffa002

View File

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