Add server CRUD with persistence and fix action mappings endpoint
- Implement complete server CRUD operations with GeViServer persistence
- POST /api/v1/configuration/servers - Create new server
- PUT /api/v1/configuration/servers/{server_id} - Update server
- DELETE /api/v1/configuration/servers/{server_id} - Delete server
- GET /api/v1/configuration/servers - List all servers
- GET /api/v1/configuration/servers/{server_id} - Get single server
- Add write_configuration_tree method to SDK bridge client
- Converts tree to JSON and writes via import_configuration
- Enables read-modify-write pattern for configuration changes
- Fix action mappings endpoint schema mismatch
- Transform response to match ActionMappingListResponse schema
- Add total_mappings, mappings_with_parameters fields
- Include id and offset in mapping responses
- Streamline configuration router
- Remove heavy endpoints (export, import, modify)
- Optimize tree navigation with depth limiting
- Add path-based configuration access
- Update OpenAPI specification with all endpoints
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
460
src/api/routers/configuration.py
Normal file
460
src/api/routers/configuration.py
Normal file
@@ -0,0 +1,460 @@
|
||||
"""
|
||||
Configuration router for GeViSoft configuration management
|
||||
Streamlined for external app integration
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, status, HTTPException
|
||||
from fastapi.responses import JSONResponse
|
||||
import structlog
|
||||
|
||||
from schemas.action_mapping_config import (
|
||||
ActionMappingResponse,
|
||||
ActionMappingListResponse,
|
||||
ActionMappingCreate,
|
||||
ActionMappingUpdate,
|
||||
ActionMappingOperationResponse
|
||||
)
|
||||
from services.configuration_service import ConfigurationService
|
||||
from middleware.auth_middleware import require_administrator, require_viewer
|
||||
from models.user import User
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/api/v1/configuration",
|
||||
tags=["configuration"]
|
||||
)
|
||||
|
||||
|
||||
# ============ CONFIGURATION TREE NAVIGATION ============
|
||||
|
||||
@router.get(
|
||||
"",
|
||||
status_code=status.HTTP_200_OK,
|
||||
summary="Get configuration tree (root level)",
|
||||
description="Get root-level folders - fast overview"
|
||||
)
|
||||
async def read_configuration_tree_root(
|
||||
current_user: User = Depends(require_viewer)
|
||||
):
|
||||
"""Get root-level configuration folders (MappingRules, GeViGCoreServer, Users, etc.)"""
|
||||
service = ConfigurationService()
|
||||
|
||||
try:
|
||||
result = await service.read_configuration_as_tree(max_depth=1)
|
||||
return JSONResponse(content=result, status_code=status.HTTP_200_OK)
|
||||
except Exception as e:
|
||||
logger.error("read_configuration_tree_root_error", error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to read configuration tree: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/path",
|
||||
status_code=status.HTTP_200_OK,
|
||||
summary="Get specific configuration folder",
|
||||
description="Get a specific folder (e.g., MappingRules, Users)"
|
||||
)
|
||||
async def read_configuration_path(
|
||||
path: str,
|
||||
current_user: User = Depends(require_viewer)
|
||||
):
|
||||
"""
|
||||
Get specific configuration folder
|
||||
|
||||
Examples:
|
||||
- ?path=MappingRules - Get all action mappings
|
||||
- ?path=GeViGCoreServer - Get all G-core servers
|
||||
- ?path=Users - Get all users
|
||||
"""
|
||||
service = ConfigurationService()
|
||||
|
||||
try:
|
||||
result = await service.read_configuration_path(path)
|
||||
return JSONResponse(content=result, status_code=status.HTTP_200_OK)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error("read_configuration_path_error", path=path, error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to read configuration path: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
# ============ ACTION MAPPINGS CRUD ============
|
||||
|
||||
@router.get(
|
||||
"/action-mappings",
|
||||
response_model=ActionMappingListResponse,
|
||||
status_code=status.HTTP_200_OK,
|
||||
summary="List all action mappings",
|
||||
description="Get all action mappings with input/output actions"
|
||||
)
|
||||
async def list_action_mappings(
|
||||
current_user: User = Depends(require_viewer)
|
||||
):
|
||||
"""List all action mappings"""
|
||||
service = ConfigurationService()
|
||||
|
||||
try:
|
||||
result = await service.read_action_mappings()
|
||||
|
||||
if not result["success"]:
|
||||
raise ValueError(result.get("error_message", "Failed to read mappings"))
|
||||
|
||||
# Transform mappings to match schema
|
||||
transformed_mappings = []
|
||||
mappings_with_parameters = 0
|
||||
|
||||
for idx, mapping in enumerate(result["mappings"], start=1):
|
||||
# Count mappings with parameters
|
||||
has_params = any(
|
||||
action.get("parameters") and len(action["parameters"]) > 0
|
||||
for action in mapping.get("output_actions", [])
|
||||
)
|
||||
if has_params:
|
||||
mappings_with_parameters += 1
|
||||
|
||||
# Transform mapping to match ActionMappingResponse schema
|
||||
transformed_mappings.append({
|
||||
"id": idx,
|
||||
"offset": mapping.get("start_offset", 0),
|
||||
"name": mapping.get("name"),
|
||||
"input_actions": mapping.get("input_actions", []),
|
||||
"output_actions": mapping.get("output_actions", [])
|
||||
})
|
||||
|
||||
return {
|
||||
"total_mappings": result["total_count"],
|
||||
"mappings_with_parameters": mappings_with_parameters,
|
||||
"mappings": transformed_mappings
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error("list_action_mappings_error", error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to list action mappings: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/action-mappings/{mapping_id}",
|
||||
response_model=ActionMappingResponse,
|
||||
status_code=status.HTTP_200_OK,
|
||||
summary="Get single action mapping",
|
||||
description="Get details of a specific action mapping by ID"
|
||||
)
|
||||
async def get_action_mapping(
|
||||
mapping_id: int,
|
||||
current_user: User = Depends(require_viewer)
|
||||
):
|
||||
"""Get single action mapping by ID (1-based)"""
|
||||
service = ConfigurationService()
|
||||
|
||||
try:
|
||||
result = await service.read_action_mappings()
|
||||
|
||||
if not result["success"]:
|
||||
raise ValueError(result.get("error_message"))
|
||||
|
||||
mappings = result.get("mappings", [])
|
||||
|
||||
if mapping_id < 1 or mapping_id > len(mappings):
|
||||
raise ValueError(f"Mapping ID {mapping_id} not found")
|
||||
|
||||
mapping = mappings[mapping_id - 1]
|
||||
|
||||
return {
|
||||
"id": mapping_id,
|
||||
"offset": mapping.get("start_offset", 0),
|
||||
"name": mapping.get("name"),
|
||||
"input_actions": mapping.get("input_actions", []),
|
||||
"output_actions": mapping.get("output_actions", [])
|
||||
}
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error("get_action_mapping_error", mapping_id=mapping_id, error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to get action mapping: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/action-mappings",
|
||||
response_model=ActionMappingOperationResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
summary="Create action mapping",
|
||||
description="Create a new action mapping"
|
||||
)
|
||||
async def create_action_mapping(
|
||||
mapping_data: ActionMappingCreate,
|
||||
current_user: User = Depends(require_administrator)
|
||||
):
|
||||
"""Create new action mapping"""
|
||||
service = ConfigurationService()
|
||||
|
||||
try:
|
||||
result = await service.create_action_mapping({
|
||||
"name": mapping_data.name,
|
||||
"output_actions": [
|
||||
{"action": action.action, "parameters": {}}
|
||||
for action in mapping_data.output_actions
|
||||
]
|
||||
})
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("create_action_mapping_error", error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to create action mapping: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.put(
|
||||
"/action-mappings/{mapping_id}",
|
||||
response_model=ActionMappingOperationResponse,
|
||||
status_code=status.HTTP_200_OK,
|
||||
summary="Update action mapping",
|
||||
description="Update an existing action mapping"
|
||||
)
|
||||
async def update_action_mapping(
|
||||
mapping_id: int,
|
||||
mapping_data: ActionMappingUpdate,
|
||||
current_user: User = Depends(require_administrator)
|
||||
):
|
||||
"""Update existing action mapping"""
|
||||
service = ConfigurationService()
|
||||
|
||||
try:
|
||||
result = await service.update_action_mapping(mapping_id, {
|
||||
"name": mapping_data.name,
|
||||
"output_actions": [
|
||||
{"action": action.action, "parameters": {}}
|
||||
for action in mapping_data.output_actions
|
||||
] if mapping_data.output_actions else None
|
||||
})
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("update_action_mapping_error", mapping_id=mapping_id, error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to update action mapping: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/action-mappings/{mapping_id}",
|
||||
response_model=ActionMappingOperationResponse,
|
||||
status_code=status.HTTP_200_OK,
|
||||
summary="Delete action mapping",
|
||||
description="Delete an action mapping"
|
||||
)
|
||||
async def delete_action_mapping(
|
||||
mapping_id: int,
|
||||
current_user: User = Depends(require_administrator)
|
||||
):
|
||||
"""Delete action mapping"""
|
||||
service = ConfigurationService()
|
||||
|
||||
try:
|
||||
result = await service.delete_action_mapping(mapping_id)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("delete_action_mapping_error", mapping_id=mapping_id, error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to delete action mapping: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
# ============ SERVER CONFIGURATION (G-CORE & GSC) ============
|
||||
|
||||
@router.get(
|
||||
"/servers",
|
||||
status_code=status.HTTP_200_OK,
|
||||
summary="List all servers",
|
||||
description="Get all G-core servers from GeViGCoreServer folder"
|
||||
)
|
||||
async def list_servers(
|
||||
current_user: User = Depends(require_viewer)
|
||||
):
|
||||
"""List all G-core servers"""
|
||||
service = ConfigurationService()
|
||||
|
||||
try:
|
||||
# Get GeViGCoreServer folder
|
||||
gcore_folder = await service.read_configuration_path("GeViGCoreServer")
|
||||
|
||||
servers = []
|
||||
if gcore_folder.get("type") == "folder" and "children" in gcore_folder:
|
||||
for child in gcore_folder["children"]:
|
||||
if child.get("type") != "folder":
|
||||
continue
|
||||
|
||||
# Extract server details
|
||||
server_id = child.get("name")
|
||||
children_dict = {c.get("name"): c for c in child.get("children", [])}
|
||||
|
||||
server = {
|
||||
"id": server_id,
|
||||
"alias": children_dict.get("Alias", {}).get("value", ""),
|
||||
"host": children_dict.get("Host", {}).get("value", ""),
|
||||
"user": children_dict.get("User", {}).get("value", ""),
|
||||
"password": children_dict.get("Password", {}).get("value", ""),
|
||||
"enabled": bool(children_dict.get("Enabled", {}).get("value", 0)),
|
||||
"deactivateEcho": bool(children_dict.get("DeactivateEcho", {}).get("value", 0)),
|
||||
"deactivateLiveCheck": bool(children_dict.get("DeactivateLiveCheck", {}).get("value", 0))
|
||||
}
|
||||
servers.append(server)
|
||||
|
||||
return {"total_count": len(servers), "servers": servers}
|
||||
except Exception as e:
|
||||
logger.error("list_servers_error", error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to list servers: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/servers/{server_id}",
|
||||
status_code=status.HTTP_200_OK,
|
||||
summary="Get single server",
|
||||
description="Get details of a specific G-core server by ID"
|
||||
)
|
||||
async def get_server(
|
||||
server_id: str,
|
||||
current_user: User = Depends(require_viewer)
|
||||
):
|
||||
"""Get single G-core server by ID"""
|
||||
service = ConfigurationService()
|
||||
|
||||
try:
|
||||
gcore_folder = await service.read_configuration_path("GeViGCoreServer")
|
||||
|
||||
if gcore_folder.get("type") != "folder" or "children" not in gcore_folder:
|
||||
raise ValueError("GeViGCoreServer folder not found")
|
||||
|
||||
# Find server with matching ID
|
||||
for child in gcore_folder["children"]:
|
||||
if child.get("type") == "folder" and child.get("name") == server_id:
|
||||
children_dict = {c.get("name"): c for c in child.get("children", [])}
|
||||
|
||||
server = {
|
||||
"id": server_id,
|
||||
"alias": children_dict.get("Alias", {}).get("value", ""),
|
||||
"host": children_dict.get("Host", {}).get("value", ""),
|
||||
"user": children_dict.get("User", {}).get("value", ""),
|
||||
"password": children_dict.get("Password", {}).get("value", ""),
|
||||
"enabled": bool(children_dict.get("Enabled", {}).get("value", 0)),
|
||||
"deactivateEcho": bool(children_dict.get("DeactivateEcho", {}).get("value", 0)),
|
||||
"deactivateLiveCheck": bool(children_dict.get("DeactivateLiveCheck", {}).get("value", 0))
|
||||
}
|
||||
return server
|
||||
|
||||
raise ValueError(f"Server '{server_id}' not found")
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error("get_server_error", server_id=server_id, error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to get server: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/servers",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
summary="Create server",
|
||||
description="Create a new G-core server"
|
||||
)
|
||||
async def create_server(
|
||||
server_data: dict,
|
||||
current_user: User = Depends(require_administrator)
|
||||
):
|
||||
"""
|
||||
Create new G-core server
|
||||
|
||||
Request body:
|
||||
{
|
||||
"id": "server-name",
|
||||
"alias": "My Server",
|
||||
"host": "192.168.1.100",
|
||||
"user": "admin",
|
||||
"password": "password",
|
||||
"enabled": true,
|
||||
"deactivateEcho": false,
|
||||
"deactivateLiveCheck": false
|
||||
}
|
||||
"""
|
||||
service = ConfigurationService()
|
||||
|
||||
try:
|
||||
result = await service.create_server(server_data)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("create_server_error", error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to create server: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.put(
|
||||
"/servers/{server_id}",
|
||||
status_code=status.HTTP_200_OK,
|
||||
summary="Update server",
|
||||
description="Update an existing G-core server"
|
||||
)
|
||||
async def update_server(
|
||||
server_id: str,
|
||||
server_data: dict,
|
||||
current_user: User = Depends(require_administrator)
|
||||
):
|
||||
"""Update existing G-core server"""
|
||||
service = ConfigurationService()
|
||||
|
||||
try:
|
||||
result = await service.update_server(server_id, server_data)
|
||||
return result
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error("update_server_error", server_id=server_id, error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to update server: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/servers/{server_id}",
|
||||
status_code=status.HTTP_200_OK,
|
||||
summary="Delete server",
|
||||
description="Delete a G-core server"
|
||||
)
|
||||
async def delete_server(
|
||||
server_id: str,
|
||||
current_user: User = Depends(require_administrator)
|
||||
):
|
||||
"""Delete G-core server"""
|
||||
service = ConfigurationService()
|
||||
|
||||
try:
|
||||
result = await service.delete_server(server_id)
|
||||
return result
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error("delete_server_error", server_id=server_id, error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to delete server: {str(e)}"
|
||||
)
|
||||
Reference in New Issue
Block a user