mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-10-26 17:41:58 -06:00
326 lines
11 KiB
Python
326 lines
11 KiB
Python
from __future__ import annotations
|
|
|
|
from ipaddress import ip_address
|
|
|
|
from flask import Blueprint, Flask, current_app, jsonify, request, session
|
|
|
|
from Data.Engine.services.container import EngineServiceContainer
|
|
from Data.Engine.services.devices import DeviceDescriptionError, RemoteDeviceError
|
|
|
|
blueprint = Blueprint("engine_devices", __name__)
|
|
|
|
|
|
def register(app: Flask, _services: EngineServiceContainer) -> None:
|
|
if "engine_devices" not in app.blueprints:
|
|
app.register_blueprint(blueprint)
|
|
|
|
|
|
def _services() -> EngineServiceContainer:
|
|
services = current_app.extensions.get("engine_services")
|
|
if services is None: # pragma: no cover - defensive
|
|
raise RuntimeError("engine services not initialized")
|
|
return services
|
|
|
|
|
|
def _inventory():
|
|
return _services().device_inventory
|
|
|
|
|
|
def _views():
|
|
return _services().device_view_service
|
|
|
|
|
|
def _require_admin():
|
|
username = session.get("username")
|
|
role = (session.get("role") or "").strip().lower()
|
|
if not isinstance(username, str) or not username:
|
|
return jsonify({"error": "not_authenticated"}), 401
|
|
if role != "admin":
|
|
return jsonify({"error": "forbidden"}), 403
|
|
return None
|
|
|
|
|
|
def _is_internal_request(req: request) -> bool:
|
|
remote = (req.remote_addr or "").strip()
|
|
if not remote:
|
|
return False
|
|
try:
|
|
return ip_address(remote).is_loopback
|
|
except ValueError:
|
|
return remote in {"localhost"}
|
|
|
|
|
|
@blueprint.route("/api/devices", methods=["GET"])
|
|
def list_devices() -> object:
|
|
devices = _inventory().list_devices()
|
|
return jsonify({"devices": devices})
|
|
|
|
|
|
@blueprint.route("/api/devices/<guid>", methods=["GET"])
|
|
def get_device_by_guid(guid: str) -> object:
|
|
device = _inventory().get_device_by_guid(guid)
|
|
if not device:
|
|
return jsonify({"error": "not found"}), 404
|
|
return jsonify(device)
|
|
|
|
|
|
@blueprint.route("/api/device/details/<hostname>", methods=["GET"])
|
|
def get_device_details(hostname: str) -> object:
|
|
payload = _inventory().get_device_details(hostname)
|
|
return jsonify(payload)
|
|
|
|
|
|
@blueprint.route("/api/device/description/<hostname>", methods=["POST"])
|
|
def set_device_description(hostname: str) -> object:
|
|
payload = request.get_json(silent=True) or {}
|
|
description = payload.get("description")
|
|
try:
|
|
_inventory().update_device_description(hostname, description)
|
|
except DeviceDescriptionError as exc:
|
|
if exc.code == "invalid_hostname":
|
|
return jsonify({"error": "invalid hostname"}), 400
|
|
if exc.code == "not_found":
|
|
return jsonify({"error": "not found"}), 404
|
|
current_app.logger.exception(
|
|
"device-description-error host=%s code=%s", hostname, exc.code
|
|
)
|
|
return jsonify({"error": "internal error"}), 500
|
|
return jsonify({"status": "ok"})
|
|
|
|
|
|
@blueprint.route("/api/agent_devices", methods=["GET"])
|
|
def list_agent_devices() -> object:
|
|
guard = _require_admin()
|
|
if guard:
|
|
return guard
|
|
devices = _inventory().list_agent_devices()
|
|
return jsonify({"devices": devices})
|
|
|
|
|
|
@blueprint.route("/api/ssh_devices", methods=["GET", "POST"])
|
|
def ssh_devices() -> object:
|
|
return _remote_devices_endpoint("ssh")
|
|
|
|
|
|
@blueprint.route("/api/winrm_devices", methods=["GET", "POST"])
|
|
def winrm_devices() -> object:
|
|
return _remote_devices_endpoint("winrm")
|
|
|
|
|
|
@blueprint.route("/api/ssh_devices/<hostname>", methods=["PUT", "DELETE"])
|
|
def ssh_device_detail(hostname: str) -> object:
|
|
return _remote_device_detail("ssh", hostname)
|
|
|
|
|
|
@blueprint.route("/api/winrm_devices/<hostname>", methods=["PUT", "DELETE"])
|
|
def winrm_device_detail(hostname: str) -> object:
|
|
return _remote_device_detail("winrm", hostname)
|
|
|
|
|
|
@blueprint.route("/api/agent/hash_list", methods=["GET"])
|
|
def agent_hash_list() -> object:
|
|
if not _is_internal_request(request):
|
|
remote_addr = (request.remote_addr or "unknown").strip() or "unknown"
|
|
current_app.logger.warning(
|
|
"/api/agent/hash_list denied non-local request from %s", remote_addr
|
|
)
|
|
return jsonify({"error": "forbidden"}), 403
|
|
try:
|
|
records = _inventory().collect_agent_hash_records()
|
|
except Exception as exc: # pragma: no cover - defensive logging
|
|
current_app.logger.exception("/api/agent/hash_list error: %s", exc)
|
|
return jsonify({"error": "internal error"}), 500
|
|
return jsonify({"agents": records})
|
|
|
|
|
|
@blueprint.route("/api/device_list_views", methods=["GET"])
|
|
def list_device_list_views() -> object:
|
|
views = _views().list_views()
|
|
return jsonify({"views": [view.to_dict() for view in views]})
|
|
|
|
|
|
@blueprint.route("/api/device_list_views/<int:view_id>", methods=["GET"])
|
|
def get_device_list_view(view_id: int) -> object:
|
|
view = _views().get_view(view_id)
|
|
if not view:
|
|
return jsonify({"error": "not found"}), 404
|
|
return jsonify(view.to_dict())
|
|
|
|
|
|
@blueprint.route("/api/device_list_views", methods=["POST"])
|
|
def create_device_list_view() -> object:
|
|
payload = request.get_json(silent=True) or {}
|
|
name = (payload.get("name") or "").strip()
|
|
columns = payload.get("columns") or []
|
|
filters = payload.get("filters") or {}
|
|
|
|
if not name:
|
|
return jsonify({"error": "name is required"}), 400
|
|
if name.lower() == "default view":
|
|
return jsonify({"error": "reserved name"}), 400
|
|
if not isinstance(columns, list) or not all(isinstance(x, str) for x in columns):
|
|
return jsonify({"error": "columns must be a list of strings"}), 400
|
|
if not isinstance(filters, dict):
|
|
return jsonify({"error": "filters must be an object"}), 400
|
|
|
|
try:
|
|
view = _views().create_view(name, columns, filters)
|
|
except ValueError as exc:
|
|
if str(exc) == "duplicate":
|
|
return jsonify({"error": "name already exists"}), 409
|
|
raise
|
|
response = jsonify(view.to_dict())
|
|
response.status_code = 201
|
|
return response
|
|
|
|
|
|
@blueprint.route("/api/device_list_views/<int:view_id>", methods=["PUT"])
|
|
def update_device_list_view(view_id: int) -> object:
|
|
payload = request.get_json(silent=True) or {}
|
|
updates: dict = {}
|
|
if "name" in payload:
|
|
name_val = payload.get("name")
|
|
if name_val is None:
|
|
return jsonify({"error": "name cannot be empty"}), 400
|
|
normalized = (str(name_val) or "").strip()
|
|
if not normalized:
|
|
return jsonify({"error": "name cannot be empty"}), 400
|
|
if normalized.lower() == "default view":
|
|
return jsonify({"error": "reserved name"}), 400
|
|
updates["name"] = normalized
|
|
if "columns" in payload:
|
|
columns_val = payload.get("columns")
|
|
if not isinstance(columns_val, list) or not all(isinstance(x, str) for x in columns_val):
|
|
return jsonify({"error": "columns must be a list of strings"}), 400
|
|
updates["columns"] = columns_val
|
|
if "filters" in payload:
|
|
filters_val = payload.get("filters")
|
|
if filters_val is not None and not isinstance(filters_val, dict):
|
|
return jsonify({"error": "filters must be an object"}), 400
|
|
if filters_val is not None:
|
|
updates["filters"] = filters_val
|
|
if not updates:
|
|
return jsonify({"error": "no fields to update"}), 400
|
|
|
|
try:
|
|
view = _views().update_view(
|
|
view_id,
|
|
name=updates.get("name"),
|
|
columns=updates.get("columns"),
|
|
filters=updates.get("filters"),
|
|
)
|
|
except ValueError as exc:
|
|
code = str(exc)
|
|
if code == "duplicate":
|
|
return jsonify({"error": "name already exists"}), 409
|
|
if code == "missing_name":
|
|
return jsonify({"error": "name cannot be empty"}), 400
|
|
if code == "reserved":
|
|
return jsonify({"error": "reserved name"}), 400
|
|
return jsonify({"error": "invalid payload"}), 400
|
|
except LookupError:
|
|
return jsonify({"error": "not found"}), 404
|
|
return jsonify(view.to_dict())
|
|
|
|
|
|
@blueprint.route("/api/device_list_views/<int:view_id>", methods=["DELETE"])
|
|
def delete_device_list_view(view_id: int) -> object:
|
|
if not _views().delete_view(view_id):
|
|
return jsonify({"error": "not found"}), 404
|
|
return jsonify({"status": "ok"})
|
|
|
|
|
|
def _remote_devices_endpoint(connection_type: str) -> object:
|
|
guard = _require_admin()
|
|
if guard:
|
|
return guard
|
|
if request.method == "GET":
|
|
devices = _inventory().list_remote_devices(connection_type)
|
|
return jsonify({"devices": devices})
|
|
|
|
payload = request.get_json(silent=True) or {}
|
|
hostname = (payload.get("hostname") or "").strip()
|
|
address = (
|
|
payload.get("address")
|
|
or payload.get("connection_endpoint")
|
|
or payload.get("endpoint")
|
|
or payload.get("host")
|
|
)
|
|
description = payload.get("description")
|
|
os_hint = payload.get("operating_system") or payload.get("os")
|
|
|
|
if not hostname:
|
|
return jsonify({"error": "hostname is required"}), 400
|
|
if not (address or "").strip():
|
|
return jsonify({"error": "address is required"}), 400
|
|
|
|
try:
|
|
device = _inventory().upsert_remote_device(
|
|
connection_type,
|
|
hostname,
|
|
address,
|
|
description,
|
|
os_hint,
|
|
ensure_existing_type=None,
|
|
)
|
|
except RemoteDeviceError as exc:
|
|
status = 409 if exc.code in {"conflict", "address_required"} else 500
|
|
if exc.code == "conflict":
|
|
return jsonify({"error": str(exc)}), 409
|
|
if exc.code == "address_required":
|
|
return jsonify({"error": "address is required"}), 400
|
|
return jsonify({"error": str(exc)}), status
|
|
return jsonify({"device": device}), 201
|
|
|
|
|
|
def _remote_device_detail(connection_type: str, hostname: str) -> object:
|
|
guard = _require_admin()
|
|
if guard:
|
|
return guard
|
|
normalized_host = (hostname or "").strip()
|
|
if not normalized_host:
|
|
return jsonify({"error": "invalid hostname"}), 400
|
|
|
|
if request.method == "DELETE":
|
|
try:
|
|
_inventory().delete_remote_device(connection_type, normalized_host)
|
|
except RemoteDeviceError as exc:
|
|
if exc.code == "not_found":
|
|
return jsonify({"error": "device not found"}), 404
|
|
if exc.code == "invalid_hostname":
|
|
return jsonify({"error": "invalid hostname"}), 400
|
|
return jsonify({"error": str(exc)}), 500
|
|
return jsonify({"status": "ok"})
|
|
|
|
payload = request.get_json(silent=True) or {}
|
|
address = (
|
|
payload.get("address")
|
|
or payload.get("connection_endpoint")
|
|
or payload.get("endpoint")
|
|
)
|
|
description = payload.get("description")
|
|
os_hint = payload.get("operating_system") or payload.get("os")
|
|
|
|
if address is None and description is None and os_hint is None:
|
|
return jsonify({"error": "no fields to update"}), 400
|
|
|
|
try:
|
|
device = _inventory().upsert_remote_device(
|
|
connection_type,
|
|
normalized_host,
|
|
address if address is not None else "",
|
|
description,
|
|
os_hint,
|
|
ensure_existing_type=connection_type,
|
|
)
|
|
except RemoteDeviceError as exc:
|
|
if exc.code == "not_found":
|
|
return jsonify({"error": "device not found"}), 404
|
|
if exc.code == "address_required":
|
|
return jsonify({"error": "address is required"}), 400
|
|
return jsonify({"error": str(exc)}), 500
|
|
return jsonify({"device": device})
|
|
|
|
|
|
__all__ = ["register", "blueprint"]
|