CRITICAL FIX: Changed boolean fields from int32 to bool type - Enabled, DeactivateEcho, DeactivateLiveCheck now use proper bool type (type code 1) - Previous int32 implementation (type code 4) caused servers to be written but not recognized by GeViSet - Fixed field order to match working reference implementation Server CRUD Implementation: - Create, Read, Update, Delete operations via gRPC and REST API - Auto-increment server ID logic to prevent conflicts - Proper field ordering: Alias, DeactivateEcho, DeactivateLiveCheck, Enabled, Host, Password, User Files Added/Modified: - src/sdk-bridge/GeViScopeBridge/Services/ConfigurationServiceImplementation.cs (bool type fix, CRUD methods) - src/sdk-bridge/Protos/configuration.proto (protocol definitions) - src/api/routers/configuration.py (REST endpoints) - src/api/protos/ (generated protobuf files) - SERVER_CRUD_IMPLEMENTATION.md (comprehensive documentation) Verified: - Servers persist correctly in GeViSoft configuration - Servers visible in GeViSet with correct boolean values - Action mappings CRUD functional - All test scripts working (server_manager.py, cleanup_to_base.py, add_claude_test_data.py) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
461 lines
15 KiB
Python
461 lines
15 KiB
Python
"""
|
|
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) # Temporarily disabled for testing
|
|
):
|
|
"""
|
|
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)}"
|
|
)
|