mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2026-01-22 13:48:43 -05:00
New Features: - API endpoints now support comprehensive input validation with detailed error responses via Pydantic models. - OpenAPI specification endpoint (/openapi.json) and interactive Swagger UI documentation (/docs) now available for API discovery. - Enhanced MCP session lifecycle management with create, retrieve, and delete operations. - Network diagnostic tools: traceroute, nslookup, NMAP scanning, and network topology viewing exposed via API. - Device search, filtering by status (including 'offline'), and bulk operations (copy, delete, update). - Wake-on-LAN functionality for remote device management. - Added dynamic tool disablement and status reporting. Bug Fixes: - Fixed get_tools_status in registry to correctly return boolean values instead of None for enabled tools. - Improved error handling for invalid API inputs with standardized validation responses. - Fixed OPTIONS request handling for cross-origin requests. Refactoring: - Significant refactoring of api_server_start.py to use decorator-based validation (@validate_request).
192 lines
6.3 KiB
Python
192 lines
6.3 KiB
Python
#!/usr/bin/env python
|
|
"""
|
|
NetAlertX OpenAPI Specification Generator
|
|
|
|
This module provides a registry-based approach to OpenAPI spec generation.
|
|
It converts Pydantic models to JSON Schema and assembles a complete OpenAPI 3.1 spec.
|
|
|
|
Key Features:
|
|
- Automatic Pydantic -> JSON Schema conversion
|
|
- Centralized endpoint registry
|
|
- Unique operationId enforcement
|
|
- Complete request/response schema generation
|
|
|
|
Usage:
|
|
from spec_generator import registry, generate_openapi_spec, register_tool
|
|
|
|
# Register endpoints (typically done at module load)
|
|
register_tool(
|
|
path="/devices/search",
|
|
method="POST",
|
|
operation_id="search_devices",
|
|
description="Search for devices",
|
|
request_model=DeviceSearchRequest,
|
|
response_model=DeviceSearchResponse
|
|
)
|
|
|
|
# Generate spec (called by MCP endpoint)
|
|
spec = generate_openapi_spec()
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import threading
|
|
from typing import Optional, List, Dict, Any
|
|
|
|
from .registry import (
|
|
clear_registry,
|
|
_registry,
|
|
_registry_lock,
|
|
_disabled_tools
|
|
)
|
|
from .introspection import introspect_flask_app, introspect_graphql_schema
|
|
from .schema_converter import (
|
|
build_parameters,
|
|
build_request_body,
|
|
build_responses
|
|
)
|
|
|
|
_rebuild_lock = threading.Lock()
|
|
|
|
|
|
def generate_openapi_spec(
|
|
title: str = "NetAlertX API",
|
|
version: str = "2.0.0",
|
|
description: str = "NetAlertX Network Monitoring API - MCP Compatible",
|
|
servers: Optional[List[Dict[str, str]]] = None,
|
|
flask_app: Optional[Any] = None
|
|
) -> Dict[str, Any]:
|
|
"""Assemble a complete OpenAPI specification from the registered endpoints."""
|
|
|
|
with _rebuild_lock:
|
|
# If no app provided and registry is empty, try to use the one from api_server_start
|
|
if not flask_app and not _registry:
|
|
try:
|
|
from ..api_server_start import app as start_app
|
|
flask_app = start_app
|
|
except (ImportError, AttributeError):
|
|
pass
|
|
|
|
# If we are in "dynamic mode", we rebuild the registry from code
|
|
if flask_app:
|
|
from ..graphql_endpoint import devicesSchema
|
|
clear_registry()
|
|
introspect_graphql_schema(devicesSchema)
|
|
introspect_flask_app(flask_app)
|
|
|
|
spec = {
|
|
"openapi": "3.1.0",
|
|
"info": {
|
|
"title": title,
|
|
"version": version,
|
|
"description": description,
|
|
"contact": {
|
|
"name": "NetAlertX",
|
|
"url": "https://github.com/jokob-sk/NetAlertX"
|
|
}
|
|
},
|
|
"servers": servers or [{"url": "/", "description": "Local server"}],
|
|
"security": [
|
|
{"BearerAuth": []}
|
|
],
|
|
"components": {
|
|
"securitySchemes": {
|
|
"BearerAuth": {
|
|
"type": "http",
|
|
"scheme": "bearer",
|
|
"description": "API token from NetAlertX settings (API_TOKEN)"
|
|
}
|
|
},
|
|
"schemas": {}
|
|
},
|
|
"paths": {},
|
|
"tags": []
|
|
}
|
|
|
|
definitions = {}
|
|
|
|
# Collect unique tags
|
|
tag_set = set()
|
|
|
|
with _registry_lock:
|
|
disabled_snapshot = _disabled_tools.copy()
|
|
for entry in _registry:
|
|
path = entry["path"]
|
|
method = entry["method"].lower()
|
|
|
|
# Initialize path if not exists
|
|
if path not in spec["paths"]:
|
|
spec["paths"][path] = {}
|
|
|
|
# Build operation object
|
|
operation = {
|
|
"operationId": entry["operation_id"],
|
|
"summary": entry["summary"],
|
|
"description": entry["description"],
|
|
"tags": entry["tags"],
|
|
"deprecated": entry["deprecated"]
|
|
}
|
|
|
|
# Inject disabled status if applicable
|
|
if entry["operation_id"] in disabled_snapshot:
|
|
operation["x-mcp-disabled"] = True
|
|
|
|
# Inject original ID if suffixed (Coderabbit fix)
|
|
if entry.get("original_operation_id"):
|
|
operation["x-original-operationId"] = entry["original_operation_id"]
|
|
|
|
# Add parameters (path + query)
|
|
parameters = build_parameters(entry)
|
|
if parameters:
|
|
operation["parameters"] = parameters
|
|
|
|
# Add request body for POST/PUT/PATCH/DELETE
|
|
if method in ("post", "put", "patch", "delete") and entry.get("request_model"):
|
|
request_body = build_request_body(
|
|
entry["request_model"],
|
|
definitions,
|
|
allow_multipart_payload=entry.get("allow_multipart_payload", False)
|
|
)
|
|
if request_body:
|
|
operation["requestBody"] = request_body
|
|
|
|
# Add responses
|
|
operation["responses"] = build_responses(
|
|
entry.get("response_model"), definitions
|
|
)
|
|
|
|
spec["paths"][path][method] = operation
|
|
|
|
# Collect tags
|
|
for tag in entry["tags"]:
|
|
tag_set.add(tag)
|
|
|
|
spec["components"]["schemas"] = definitions
|
|
|
|
# Build tags array with descriptions
|
|
tag_descriptions = {
|
|
"devices": "Device management and queries",
|
|
"nettools": "Network diagnostic tools",
|
|
"events": "Event and alert management",
|
|
"sessions": "Session history tracking",
|
|
"messaging": "In-app notifications",
|
|
"settings": "Configuration management",
|
|
"sync": "Data synchronization",
|
|
"logs": "Log file access",
|
|
"dbquery": "Direct database queries"
|
|
}
|
|
|
|
spec["tags"] = [
|
|
{"name": tag, "description": tag_descriptions.get(tag, f"{tag.title()} operations")}
|
|
for tag in sorted(tag_set)
|
|
]
|
|
|
|
return spec
|
|
|
|
|
|
# Initialize registry on module load
|
|
# Registry is now populated dynamically via introspection in generate_openapi_spec
|
|
def _register_all_endpoints():
|
|
"""Dummy function for compatibility with legacy tests."""
|
|
pass
|