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:
@@ -6,10 +6,12 @@ from typing import Optional, List
|
||||
import structlog
|
||||
from config import settings
|
||||
|
||||
# TODO: Import generated protobuf classes after running protoc
|
||||
# from protos import camera_pb2, camera_pb2_grpc
|
||||
# from protos import monitor_pb2, monitor_pb2_grpc
|
||||
# from protos import crossswitch_pb2, crossswitch_pb2_grpc
|
||||
# Import generated protobuf classes
|
||||
from protos import camera_pb2, camera_pb2_grpc
|
||||
from protos import monitor_pb2, monitor_pb2_grpc
|
||||
from protos import crossswitch_pb2, crossswitch_pb2_grpc
|
||||
from protos import action_mapping_pb2, action_mapping_pb2_grpc
|
||||
from protos import configuration_pb2, configuration_pb2_grpc
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
@@ -18,10 +20,11 @@ class SDKBridgeClient:
|
||||
|
||||
def __init__(self):
|
||||
self._channel: Optional[grpc.aio.Channel] = None
|
||||
# TODO: Initialize stubs after protobuf generation
|
||||
# self._camera_stub = None
|
||||
# self._monitor_stub = None
|
||||
# self._crossswitch_stub = None
|
||||
self._camera_stub = None
|
||||
self._monitor_stub = None
|
||||
self._crossswitch_stub = None
|
||||
self._action_mapping_stub = None
|
||||
self._configuration_stub = None
|
||||
|
||||
async def connect(self):
|
||||
"""Initialize gRPC channel to SDK Bridge"""
|
||||
@@ -39,13 +42,12 @@ class SDKBridgeClient:
|
||||
]
|
||||
)
|
||||
|
||||
# TODO: Initialize service stubs after protobuf generation
|
||||
# self._camera_stub = camera_pb2_grpc.CameraServiceStub(self._channel)
|
||||
# self._monitor_stub = monitor_pb2_grpc.MonitorServiceStub(self._channel)
|
||||
# self._crossswitch_stub = crossswitch_pb2_grpc.CrossSwitchServiceStub(self._channel)
|
||||
|
||||
# Test connection with health check
|
||||
# await self.health_check()
|
||||
# Initialize service stubs
|
||||
self._camera_stub = camera_pb2_grpc.CameraServiceStub(self._channel)
|
||||
self._monitor_stub = monitor_pb2_grpc.MonitorServiceStub(self._channel)
|
||||
self._crossswitch_stub = crossswitch_pb2_grpc.CrossSwitchServiceStub(self._channel)
|
||||
self._action_mapping_stub = action_mapping_pb2_grpc.ActionMappingServiceStub(self._channel)
|
||||
self._configuration_stub = configuration_pb2_grpc.ConfigurationServiceStub(self._channel)
|
||||
|
||||
logger.info("sdk_bridge_connected")
|
||||
except Exception as e:
|
||||
@@ -82,21 +84,20 @@ class SDKBridgeClient:
|
||||
"""List all cameras from GeViServer"""
|
||||
try:
|
||||
logger.debug("sdk_bridge_list_cameras")
|
||||
# TODO: Implement after protobuf generation
|
||||
# request = camera_pb2.ListCamerasRequest()
|
||||
# response = await self._camera_stub.ListCameras(request, timeout=10.0)
|
||||
# return [
|
||||
# {
|
||||
# "id": camera.id,
|
||||
# "name": camera.name,
|
||||
# "description": camera.description,
|
||||
# "has_ptz": camera.has_ptz,
|
||||
# "has_video_sensor": camera.has_video_sensor,
|
||||
# "status": camera.status
|
||||
# }
|
||||
# for camera in response.cameras
|
||||
# ]
|
||||
return [] # Placeholder
|
||||
request = camera_pb2.ListCamerasRequest()
|
||||
response = await self._camera_stub.ListCameras(request, timeout=10.0)
|
||||
return [
|
||||
{
|
||||
"id": camera.id,
|
||||
"name": camera.name,
|
||||
"description": camera.description,
|
||||
"has_ptz": camera.has_ptz,
|
||||
"has_video_sensor": camera.has_video_sensor,
|
||||
"status": camera.status,
|
||||
"last_seen": None # TODO: Convert protobuf timestamp to datetime
|
||||
}
|
||||
for camera in response.cameras
|
||||
]
|
||||
except grpc.RpcError as e:
|
||||
logger.error("sdk_bridge_list_cameras_failed", error=str(e))
|
||||
raise
|
||||
@@ -127,21 +128,19 @@ class SDKBridgeClient:
|
||||
"""List all monitors from GeViServer"""
|
||||
try:
|
||||
logger.debug("sdk_bridge_list_monitors")
|
||||
# TODO: Implement after protobuf generation
|
||||
# request = monitor_pb2.ListMonitorsRequest()
|
||||
# response = await self._monitor_stub.ListMonitors(request, timeout=10.0)
|
||||
# return [
|
||||
# {
|
||||
# "id": monitor.id,
|
||||
# "name": monitor.name,
|
||||
# "description": monitor.description,
|
||||
# "is_active": monitor.is_active,
|
||||
# "current_camera_id": monitor.current_camera_id,
|
||||
# "status": monitor.status
|
||||
# }
|
||||
# for monitor in response.monitors
|
||||
# ]
|
||||
return [] # Placeholder
|
||||
request = monitor_pb2.ListMonitorsRequest()
|
||||
response = await self._monitor_stub.ListMonitors(request, timeout=10.0)
|
||||
return [
|
||||
{
|
||||
"id": monitor.id,
|
||||
"name": monitor.name,
|
||||
"description": monitor.description,
|
||||
"is_active": monitor.is_active,
|
||||
"current_camera_id": monitor.current_camera_id,
|
||||
"status": monitor.status
|
||||
}
|
||||
for monitor in response.monitors
|
||||
]
|
||||
except grpc.RpcError as e:
|
||||
logger.error("sdk_bridge_list_monitors_failed", error=str(e))
|
||||
raise
|
||||
@@ -150,20 +149,18 @@ class SDKBridgeClient:
|
||||
"""Execute cross-switch operation"""
|
||||
try:
|
||||
logger.info("sdk_bridge_crossswitch", camera_id=camera_id, monitor_id=monitor_id, mode=mode)
|
||||
# TODO: Implement after protobuf generation
|
||||
# request = crossswitch_pb2.CrossSwitchRequest(
|
||||
# camera_id=camera_id,
|
||||
# monitor_id=monitor_id,
|
||||
# mode=mode
|
||||
# )
|
||||
# response = await self._crossswitch_stub.ExecuteCrossSwitch(request, timeout=10.0)
|
||||
# return {
|
||||
# "success": response.success,
|
||||
# "message": response.message,
|
||||
# "camera_id": response.camera_id,
|
||||
# "monitor_id": response.monitor_id
|
||||
# }
|
||||
return {"success": True, "message": "Placeholder", "camera_id": camera_id, "monitor_id": monitor_id}
|
||||
request = crossswitch_pb2.CrossSwitchRequest(
|
||||
camera_id=camera_id,
|
||||
monitor_id=monitor_id,
|
||||
mode=mode
|
||||
)
|
||||
response = await self._crossswitch_stub.ExecuteCrossSwitch(request, timeout=10.0)
|
||||
return {
|
||||
"success": response.success,
|
||||
"message": response.message,
|
||||
"camera_id": response.camera_id,
|
||||
"monitor_id": response.monitor_id
|
||||
}
|
||||
except grpc.RpcError as e:
|
||||
logger.error("sdk_bridge_crossswitch_failed", error=str(e))
|
||||
raise
|
||||
@@ -172,15 +169,13 @@ class SDKBridgeClient:
|
||||
"""Clear monitor (stop video)"""
|
||||
try:
|
||||
logger.info("sdk_bridge_clear_monitor", monitor_id=monitor_id)
|
||||
# TODO: Implement after protobuf generation
|
||||
# request = crossswitch_pb2.ClearMonitorRequest(monitor_id=monitor_id)
|
||||
# response = await self._crossswitch_stub.ClearMonitor(request, timeout=10.0)
|
||||
# return {
|
||||
# "success": response.success,
|
||||
# "message": response.message,
|
||||
# "monitor_id": response.monitor_id
|
||||
# }
|
||||
return {"success": True, "message": "Placeholder", "monitor_id": monitor_id}
|
||||
request = crossswitch_pb2.ClearMonitorRequest(monitor_id=monitor_id)
|
||||
response = await self._crossswitch_stub.ClearMonitor(request, timeout=10.0)
|
||||
return {
|
||||
"success": response.success,
|
||||
"message": response.message,
|
||||
"monitor_id": response.monitor_id
|
||||
}
|
||||
except grpc.RpcError as e:
|
||||
logger.error("sdk_bridge_clear_monitor_failed", error=str(e))
|
||||
raise
|
||||
@@ -209,6 +204,451 @@ class SDKBridgeClient:
|
||||
logger.error("sdk_bridge_get_routing_state_failed", error=str(e))
|
||||
raise
|
||||
|
||||
async def get_action_mappings(self, enabled_only: bool = False) -> dict:
|
||||
"""Get action mappings from GeViServer via SDK Bridge"""
|
||||
try:
|
||||
logger.debug("sdk_bridge_get_action_mappings", enabled_only=enabled_only)
|
||||
request = action_mapping_pb2.GetActionMappingsRequest(enabled_only=enabled_only)
|
||||
response = await self._action_mapping_stub.GetActionMappings(request, timeout=30.0)
|
||||
|
||||
return {
|
||||
"mappings": [
|
||||
{
|
||||
"id": mapping.id,
|
||||
"name": mapping.name,
|
||||
"description": mapping.description,
|
||||
"input_action": mapping.input_action,
|
||||
"output_actions": list(mapping.output_actions),
|
||||
"enabled": mapping.enabled,
|
||||
"execution_count": mapping.execution_count,
|
||||
"last_executed": mapping.last_executed if mapping.last_executed else None,
|
||||
"created_at": mapping.created_at,
|
||||
"updated_at": mapping.updated_at
|
||||
}
|
||||
for mapping in response.mappings
|
||||
],
|
||||
"total_count": response.total_count,
|
||||
"enabled_count": response.enabled_count,
|
||||
"disabled_count": response.disabled_count
|
||||
}
|
||||
except grpc.RpcError as e:
|
||||
logger.error("sdk_bridge_get_action_mappings_failed", error=str(e))
|
||||
raise
|
||||
|
||||
async def read_configuration(self) -> dict:
|
||||
"""Read and parse configuration from GeViServer"""
|
||||
try:
|
||||
logger.debug("sdk_bridge_read_configuration")
|
||||
request = configuration_pb2.ReadConfigurationRequest()
|
||||
response = await self._configuration_stub.ReadConfiguration(request, timeout=30.0)
|
||||
|
||||
return {
|
||||
"success": response.success,
|
||||
"error_message": response.error_message if response.error_message else None,
|
||||
"file_size": response.file_size,
|
||||
"header": response.header,
|
||||
"nodes": [
|
||||
{
|
||||
"start_offset": node.start_offset,
|
||||
"end_offset": node.end_offset,
|
||||
"node_type": node.node_type,
|
||||
"name": node.name if node.name else None,
|
||||
"value": node.value if node.value else None,
|
||||
"value_type": node.value_type if node.value_type else None
|
||||
}
|
||||
for node in response.nodes
|
||||
],
|
||||
"statistics": {
|
||||
"total_nodes": response.statistics.total_nodes,
|
||||
"boolean_count": response.statistics.boolean_count,
|
||||
"integer_count": response.statistics.integer_count,
|
||||
"string_count": response.statistics.string_count,
|
||||
"property_count": response.statistics.property_count,
|
||||
"marker_count": response.statistics.marker_count,
|
||||
"rules_section_count": response.statistics.rules_section_count
|
||||
}
|
||||
}
|
||||
except grpc.RpcError as e:
|
||||
logger.error("sdk_bridge_read_configuration_failed", error=str(e))
|
||||
raise
|
||||
|
||||
async def export_configuration_json(self) -> dict:
|
||||
"""Export configuration as JSON"""
|
||||
try:
|
||||
logger.debug("sdk_bridge_export_configuration_json")
|
||||
request = configuration_pb2.ExportJsonRequest()
|
||||
response = await self._configuration_stub.ExportConfigurationJson(request, timeout=30.0)
|
||||
|
||||
return {
|
||||
"success": response.success,
|
||||
"error_message": response.error_message if response.error_message else None,
|
||||
"json_data": response.json_data,
|
||||
"json_size": response.json_size
|
||||
}
|
||||
except grpc.RpcError as e:
|
||||
logger.error("sdk_bridge_export_configuration_json_failed", error=str(e))
|
||||
raise
|
||||
|
||||
async def modify_configuration(self, modifications: List[dict]) -> dict:
|
||||
"""Modify configuration and write back to server"""
|
||||
try:
|
||||
logger.info("sdk_bridge_modify_configuration", count=len(modifications))
|
||||
request = configuration_pb2.ModifyConfigurationRequest()
|
||||
|
||||
for mod in modifications:
|
||||
modification = configuration_pb2.NodeModification(
|
||||
start_offset=mod["start_offset"],
|
||||
node_type=mod["node_type"],
|
||||
new_value=mod["new_value"]
|
||||
)
|
||||
request.modifications.append(modification)
|
||||
|
||||
response = await self._configuration_stub.ModifyConfiguration(request, timeout=60.0)
|
||||
|
||||
return {
|
||||
"success": response.success,
|
||||
"error_message": response.error_message if response.error_message else None,
|
||||
"modifications_applied": response.modifications_applied
|
||||
}
|
||||
except grpc.RpcError as e:
|
||||
logger.error("sdk_bridge_modify_configuration_failed", error=str(e))
|
||||
raise
|
||||
|
||||
async def import_configuration(self, json_data: str) -> dict:
|
||||
"""Import complete configuration from JSON and write to GeViServer"""
|
||||
try:
|
||||
logger.info("sdk_bridge_import_configuration", json_size=len(json_data))
|
||||
request = configuration_pb2.ImportConfigurationRequest(json_data=json_data)
|
||||
response = await self._configuration_stub.ImportConfiguration(request, timeout=60.0)
|
||||
|
||||
return {
|
||||
"success": response.success,
|
||||
"error_message": response.error_message if response.error_message else None,
|
||||
"bytes_written": response.bytes_written,
|
||||
"nodes_imported": response.nodes_imported
|
||||
}
|
||||
except grpc.RpcError as e:
|
||||
logger.error("sdk_bridge_import_configuration_failed", error=str(e))
|
||||
raise
|
||||
|
||||
async def read_action_mappings(self) -> dict:
|
||||
"""
|
||||
Read ONLY action mappings (Rules markers) from GeViServer
|
||||
Much faster than full configuration export - selective parsing
|
||||
Returns structured format with input_actions and output_actions with parameters
|
||||
"""
|
||||
try:
|
||||
logger.info("sdk_bridge_read_action_mappings")
|
||||
request = configuration_pb2.ReadActionMappingsRequest()
|
||||
response = await self._configuration_stub.ReadActionMappings(request, timeout=30.0)
|
||||
|
||||
# Convert protobuf response to dict with structured format
|
||||
mappings = []
|
||||
for mapping in response.mappings:
|
||||
# Convert input actions with parameters
|
||||
input_actions = []
|
||||
for action_def in mapping.input_actions:
|
||||
parameters = {}
|
||||
for param in action_def.parameters:
|
||||
parameters[param.name] = param.value
|
||||
|
||||
input_actions.append({
|
||||
"action": action_def.action,
|
||||
"parameters": parameters
|
||||
})
|
||||
|
||||
# Convert output actions with parameters
|
||||
output_actions = []
|
||||
for action_def in mapping.output_actions:
|
||||
parameters = {}
|
||||
for param in action_def.parameters:
|
||||
parameters[param.name] = param.value
|
||||
|
||||
output_actions.append({
|
||||
"action": action_def.action,
|
||||
"parameters": parameters
|
||||
})
|
||||
|
||||
mappings.append({
|
||||
"name": mapping.name,
|
||||
"input_actions": input_actions,
|
||||
"output_actions": output_actions,
|
||||
"start_offset": mapping.start_offset,
|
||||
"end_offset": mapping.end_offset,
|
||||
# Keep old format for backward compatibility
|
||||
"actions": list(mapping.actions)
|
||||
})
|
||||
|
||||
return {
|
||||
"success": response.success,
|
||||
"error_message": response.error_message if response.error_message else None,
|
||||
"mappings": mappings,
|
||||
"total_count": response.total_count
|
||||
}
|
||||
|
||||
except grpc.RpcError as e:
|
||||
logger.error("sdk_bridge_read_action_mappings_failed", error=str(e))
|
||||
raise
|
||||
|
||||
async def read_specific_markers(self, marker_names: List[str]) -> dict:
|
||||
"""
|
||||
Read specific configuration markers by name
|
||||
Extensible method for reading any configuration type
|
||||
"""
|
||||
try:
|
||||
logger.info("sdk_bridge_read_specific_markers", markers=marker_names)
|
||||
request = configuration_pb2.ReadSpecificMarkersRequest(marker_names=marker_names)
|
||||
response = await self._configuration_stub.ReadSpecificMarkers(request, timeout=30.0)
|
||||
|
||||
# Convert protobuf response to dict
|
||||
nodes = []
|
||||
for node in response.extracted_nodes:
|
||||
nodes.append({
|
||||
"start_offset": node.start_offset,
|
||||
"end_offset": node.end_offset,
|
||||
"node_type": node.node_type,
|
||||
"name": node.name,
|
||||
"value": node.value,
|
||||
"value_type": node.value_type
|
||||
})
|
||||
|
||||
return {
|
||||
"success": response.success,
|
||||
"error_message": response.error_message if response.error_message else None,
|
||||
"file_size": response.file_size,
|
||||
"requested_markers": list(response.requested_markers),
|
||||
"extracted_nodes": nodes,
|
||||
"markers_found": response.markers_found
|
||||
}
|
||||
|
||||
except grpc.RpcError as e:
|
||||
logger.error("sdk_bridge_read_specific_markers_failed", error=str(e))
|
||||
raise
|
||||
|
||||
async def create_action_mapping(self, mapping_data: dict) -> dict:
|
||||
"""
|
||||
Create a new action mapping
|
||||
|
||||
Args:
|
||||
mapping_data: Dict with name, input_actions, output_actions
|
||||
|
||||
Returns:
|
||||
Dict with success status and created mapping
|
||||
"""
|
||||
try:
|
||||
logger.info("sdk_bridge_create_action_mapping", name=mapping_data.get("name"))
|
||||
|
||||
# Build protobuf request
|
||||
mapping_input = configuration_pb2.ActionMappingInput(
|
||||
name=mapping_data.get("name", "")
|
||||
)
|
||||
|
||||
# Add output actions
|
||||
for action_data in mapping_data.get("output_actions", []):
|
||||
action_def = configuration_pb2.ActionDefinition(action=action_data["action"])
|
||||
|
||||
# Add parameters
|
||||
for param_name, param_value in action_data.get("parameters", {}).items():
|
||||
action_def.parameters.add(name=param_name, value=str(param_value))
|
||||
|
||||
mapping_input.output_actions.append(action_def)
|
||||
|
||||
request = configuration_pb2.CreateActionMappingRequest(mapping=mapping_input)
|
||||
response = await self._configuration_stub.CreateActionMapping(request, timeout=60.0)
|
||||
|
||||
# Convert response
|
||||
result = {
|
||||
"success": response.success,
|
||||
"error_message": response.error_message if response.error_message else None,
|
||||
"message": response.message
|
||||
}
|
||||
|
||||
if response.mapping:
|
||||
result["mapping"] = {
|
||||
"id": len([]), # ID will be assigned by the system
|
||||
"name": response.mapping.name,
|
||||
"offset": response.mapping.start_offset,
|
||||
"output_actions": []
|
||||
}
|
||||
|
||||
for action_def in response.mapping.output_actions:
|
||||
result["mapping"]["output_actions"].append({
|
||||
"action": action_def.action,
|
||||
"parameters": {p.name: p.value for p in action_def.parameters}
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
except grpc.RpcError as e:
|
||||
logger.error("sdk_bridge_create_action_mapping_failed", error=str(e))
|
||||
raise
|
||||
|
||||
async def update_action_mapping(self, mapping_id: int, mapping_data: dict) -> dict:
|
||||
"""
|
||||
Update an existing action mapping
|
||||
|
||||
Args:
|
||||
mapping_id: 1-based ID of mapping to update
|
||||
mapping_data: Dict with updated fields (name, input_actions, output_actions)
|
||||
|
||||
Returns:
|
||||
Dict with success status and updated mapping
|
||||
"""
|
||||
try:
|
||||
logger.info("sdk_bridge_update_action_mapping", mapping_id=mapping_id)
|
||||
|
||||
# Build protobuf request
|
||||
mapping_input = configuration_pb2.ActionMappingInput()
|
||||
|
||||
if "name" in mapping_data:
|
||||
mapping_input.name = mapping_data["name"]
|
||||
|
||||
# Add output actions if provided
|
||||
if "output_actions" in mapping_data:
|
||||
for action_data in mapping_data["output_actions"]:
|
||||
action_def = configuration_pb2.ActionDefinition(action=action_data["action"])
|
||||
|
||||
# Add parameters
|
||||
for param_name, param_value in action_data.get("parameters", {}).items():
|
||||
action_def.parameters.add(name=param_name, value=str(param_value))
|
||||
|
||||
mapping_input.output_actions.append(action_def)
|
||||
|
||||
request = configuration_pb2.UpdateActionMappingRequest(
|
||||
mapping_id=mapping_id,
|
||||
mapping=mapping_input
|
||||
)
|
||||
response = await self._configuration_stub.UpdateActionMapping(request, timeout=60.0)
|
||||
|
||||
# Convert response
|
||||
result = {
|
||||
"success": response.success,
|
||||
"error_message": response.error_message if response.error_message else None,
|
||||
"message": response.message
|
||||
}
|
||||
|
||||
if response.mapping:
|
||||
result["mapping"] = {
|
||||
"id": mapping_id,
|
||||
"name": response.mapping.name,
|
||||
"offset": response.mapping.start_offset,
|
||||
"output_actions": []
|
||||
}
|
||||
|
||||
for action_def in response.mapping.output_actions:
|
||||
result["mapping"]["output_actions"].append({
|
||||
"action": action_def.action,
|
||||
"parameters": {p.name: p.value for p in action_def.parameters}
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
except grpc.RpcError as e:
|
||||
logger.error("sdk_bridge_update_action_mapping_failed", error=str(e))
|
||||
raise
|
||||
|
||||
async def delete_action_mapping(self, mapping_id: int) -> dict:
|
||||
"""
|
||||
Delete an action mapping by ID
|
||||
|
||||
Args:
|
||||
mapping_id: 1-based ID of mapping to delete
|
||||
|
||||
Returns:
|
||||
Dict with success status and message
|
||||
"""
|
||||
try:
|
||||
logger.info("sdk_bridge_delete_action_mapping", mapping_id=mapping_id)
|
||||
|
||||
request = configuration_pb2.DeleteActionMappingRequest(mapping_id=mapping_id)
|
||||
response = await self._configuration_stub.DeleteActionMapping(request, timeout=60.0)
|
||||
|
||||
return {
|
||||
"success": response.success,
|
||||
"error_message": response.error_message if response.error_message else None,
|
||||
"message": response.message
|
||||
}
|
||||
|
||||
except grpc.RpcError as e:
|
||||
logger.error("sdk_bridge_delete_action_mapping_failed", error=str(e))
|
||||
raise
|
||||
|
||||
async def read_configuration_tree(self) -> dict:
|
||||
"""
|
||||
Read configuration as hierarchical folder tree (RECOMMENDED)
|
||||
|
||||
Returns:
|
||||
Dict with tree structure
|
||||
"""
|
||||
try:
|
||||
logger.info("sdk_bridge_read_configuration_tree")
|
||||
|
||||
request = configuration_pb2.ReadConfigurationTreeRequest()
|
||||
response = await self._configuration_stub.ReadConfigurationTree(request, timeout=30.0)
|
||||
|
||||
if not response.success:
|
||||
return {
|
||||
"success": False,
|
||||
"error_message": response.error_message
|
||||
}
|
||||
|
||||
# Convert protobuf TreeNode to dict
|
||||
def convert_tree_node(node):
|
||||
result = {
|
||||
"type": node.type,
|
||||
"name": node.name
|
||||
}
|
||||
|
||||
# Add value based on type
|
||||
if node.type == "string":
|
||||
result["value"] = node.string_value
|
||||
elif node.type in ("bool", "byte", "int16", "int32", "int64"):
|
||||
result["value"] = node.int_value
|
||||
|
||||
# Add children recursively
|
||||
if node.type == "folder" and len(node.children) > 0:
|
||||
result["children"] = [convert_tree_node(child) for child in node.children]
|
||||
|
||||
return result
|
||||
|
||||
tree_dict = convert_tree_node(response.root) if response.root else None
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"tree": tree_dict,
|
||||
"total_nodes": response.total_nodes
|
||||
}
|
||||
|
||||
except grpc.RpcError as e:
|
||||
logger.error("sdk_bridge_read_configuration_tree_failed", error=str(e))
|
||||
raise
|
||||
|
||||
async def write_configuration_tree(self, tree: dict) -> dict:
|
||||
"""
|
||||
Write modified configuration tree back to GeViServer
|
||||
|
||||
Args:
|
||||
tree: Modified tree structure (dict)
|
||||
|
||||
Returns:
|
||||
Dict with success status and write statistics
|
||||
"""
|
||||
try:
|
||||
import json
|
||||
logger.info("sdk_bridge_write_configuration_tree")
|
||||
|
||||
# Convert tree to JSON string
|
||||
json_data = json.dumps(tree, indent=2)
|
||||
|
||||
# Use import_configuration to write the tree
|
||||
result = await self.import_configuration(json_data)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error("sdk_bridge_write_configuration_tree_failed", error=str(e))
|
||||
raise
|
||||
|
||||
# Global SDK Bridge client instance
|
||||
sdk_bridge_client = SDKBridgeClient()
|
||||
|
||||
|
||||
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)}"
|
||||
)
|
||||
647
src/api/services/configuration_service.py
Normal file
647
src/api/services/configuration_service.py
Normal file
@@ -0,0 +1,647 @@
|
||||
"""
|
||||
Configuration service for managing GeViSoft configuration
|
||||
"""
|
||||
from typing import Dict, Any
|
||||
import structlog
|
||||
|
||||
from clients.sdk_bridge_client import sdk_bridge_client
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
class ConfigurationService:
|
||||
"""Service for configuration operations"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize configuration service"""
|
||||
pass
|
||||
|
||||
async def read_configuration(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Read and parse complete configuration from GeViServer
|
||||
|
||||
Returns:
|
||||
Dictionary with configuration data and statistics
|
||||
"""
|
||||
try:
|
||||
logger.info("configuration_service_reading_config")
|
||||
result = await sdk_bridge_client.read_configuration()
|
||||
|
||||
if not result["success"]:
|
||||
logger.error("configuration_read_failed", error=result.get("error_message"))
|
||||
raise ValueError(f"Configuration read failed: {result.get('error_message')}")
|
||||
|
||||
logger.info("configuration_read_success",
|
||||
total_nodes=result["statistics"]["total_nodes"],
|
||||
file_size=result["file_size"])
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error("configuration_service_read_failed", error=str(e), exc_info=True)
|
||||
raise
|
||||
|
||||
async def export_configuration_json(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Export complete configuration as JSON
|
||||
|
||||
Returns:
|
||||
Dictionary with JSON data and size
|
||||
"""
|
||||
try:
|
||||
logger.info("configuration_service_exporting_json")
|
||||
result = await sdk_bridge_client.export_configuration_json()
|
||||
|
||||
if not result["success"]:
|
||||
logger.error("configuration_export_failed", error=result.get("error_message"))
|
||||
raise ValueError(f"Configuration export failed: {result.get('error_message')}")
|
||||
|
||||
logger.info("configuration_export_success", json_size=result["json_size"])
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error("configuration_service_export_failed", error=str(e), exc_info=True)
|
||||
raise
|
||||
|
||||
async def modify_configuration(self, modifications: list) -> Dict[str, Any]:
|
||||
"""
|
||||
Modify configuration values and write back to server
|
||||
|
||||
Args:
|
||||
modifications: List of modifications to apply
|
||||
|
||||
Returns:
|
||||
Dictionary with success status and count of modifications applied
|
||||
"""
|
||||
try:
|
||||
logger.info("configuration_service_modifying",
|
||||
modification_count=len(modifications))
|
||||
|
||||
result = await sdk_bridge_client.modify_configuration(modifications)
|
||||
|
||||
if not result["success"]:
|
||||
logger.error("configuration_modify_failed", error=result.get("error_message"))
|
||||
raise ValueError(f"Configuration modification failed: {result.get('error_message')}")
|
||||
|
||||
logger.info("configuration_modify_success",
|
||||
modifications_applied=result["modifications_applied"])
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error("configuration_service_modify_failed", error=str(e), exc_info=True)
|
||||
raise
|
||||
|
||||
async def import_configuration(self, json_data: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Import complete configuration from JSON and write to GeViServer
|
||||
|
||||
Args:
|
||||
json_data: Complete configuration as JSON string
|
||||
|
||||
Returns:
|
||||
Dictionary with success status, bytes written, and nodes imported
|
||||
"""
|
||||
try:
|
||||
logger.info("configuration_service_importing",
|
||||
json_size=len(json_data))
|
||||
|
||||
result = await sdk_bridge_client.import_configuration(json_data)
|
||||
|
||||
if not result["success"]:
|
||||
logger.error("configuration_import_failed", error=result.get("error_message"))
|
||||
raise ValueError(f"Configuration import failed: {result.get('error_message')}")
|
||||
|
||||
logger.info("configuration_import_success",
|
||||
bytes_written=result["bytes_written"],
|
||||
nodes_imported=result["nodes_imported"])
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error("configuration_service_import_failed", error=str(e), exc_info=True)
|
||||
raise
|
||||
|
||||
async def read_action_mappings(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Read ONLY action mappings (Rules markers) from GeViServer
|
||||
Much faster than full configuration export
|
||||
|
||||
Returns:
|
||||
Dictionary with action mappings list and count
|
||||
"""
|
||||
try:
|
||||
logger.info("configuration_service_reading_action_mappings")
|
||||
result = await sdk_bridge_client.read_action_mappings()
|
||||
|
||||
if not result["success"]:
|
||||
logger.error("action_mappings_read_failed", error=result.get("error_message"))
|
||||
raise ValueError(f"Action mappings read failed: {result.get('error_message')}")
|
||||
|
||||
logger.info("action_mappings_read_success",
|
||||
total_count=result["total_count"],
|
||||
total_actions=sum(len(m["actions"]) for m in result["mappings"]))
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error("configuration_service_read_action_mappings_failed", error=str(e), exc_info=True)
|
||||
raise
|
||||
|
||||
async def read_specific_markers(self, marker_names: list) -> Dict[str, Any]:
|
||||
"""
|
||||
Read specific configuration markers by name
|
||||
|
||||
Args:
|
||||
marker_names: List of marker names to extract (e.g., ["Rules", "Camera"])
|
||||
|
||||
Returns:
|
||||
Dictionary with extracted nodes and statistics
|
||||
"""
|
||||
try:
|
||||
logger.info("configuration_service_reading_specific_markers",
|
||||
markers=marker_names)
|
||||
result = await sdk_bridge_client.read_specific_markers(marker_names)
|
||||
|
||||
if not result["success"]:
|
||||
logger.error("specific_markers_read_failed", error=result.get("error_message"))
|
||||
raise ValueError(f"Specific markers read failed: {result.get('error_message')}")
|
||||
|
||||
logger.info("specific_markers_read_success",
|
||||
markers_found=result["markers_found"])
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error("configuration_service_read_specific_markers_failed", error=str(e), exc_info=True)
|
||||
raise
|
||||
|
||||
async def create_action_mapping(self, mapping_data: dict) -> Dict[str, Any]:
|
||||
"""
|
||||
Create a new action mapping
|
||||
|
||||
Args:
|
||||
mapping_data: Dictionary with name, input_actions, output_actions
|
||||
|
||||
Returns:
|
||||
Dictionary with success status and created mapping
|
||||
"""
|
||||
try:
|
||||
logger.info("configuration_service_creating_action_mapping",
|
||||
name=mapping_data.get("name"))
|
||||
|
||||
result = await sdk_bridge_client.create_action_mapping(mapping_data)
|
||||
|
||||
if not result["success"]:
|
||||
logger.error("action_mapping_create_failed", error=result.get("error_message"))
|
||||
raise ValueError(f"Action mapping creation failed: {result.get('error_message')}")
|
||||
|
||||
logger.info("action_mapping_create_success")
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error("configuration_service_create_action_mapping_failed", error=str(e), exc_info=True)
|
||||
raise
|
||||
|
||||
async def update_action_mapping(self, mapping_id: int, mapping_data: dict) -> Dict[str, Any]:
|
||||
"""
|
||||
Update an existing action mapping
|
||||
|
||||
Args:
|
||||
mapping_id: 1-based ID of mapping to update
|
||||
mapping_data: Dictionary with updated fields
|
||||
|
||||
Returns:
|
||||
Dictionary with success status and updated mapping
|
||||
"""
|
||||
try:
|
||||
logger.info("configuration_service_updating_action_mapping",
|
||||
mapping_id=mapping_id)
|
||||
|
||||
result = await sdk_bridge_client.update_action_mapping(mapping_id, mapping_data)
|
||||
|
||||
if not result["success"]:
|
||||
logger.error("action_mapping_update_failed", error=result.get("error_message"))
|
||||
raise ValueError(f"Action mapping update failed: {result.get('error_message')}")
|
||||
|
||||
logger.info("action_mapping_update_success", mapping_id=mapping_id)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error("configuration_service_update_action_mapping_failed", error=str(e), exc_info=True)
|
||||
raise
|
||||
|
||||
async def delete_action_mapping(self, mapping_id: int) -> Dict[str, Any]:
|
||||
"""
|
||||
Delete an action mapping by ID
|
||||
|
||||
Args:
|
||||
mapping_id: 1-based ID of mapping to delete
|
||||
|
||||
Returns:
|
||||
Dictionary with success status and message
|
||||
"""
|
||||
try:
|
||||
logger.info("configuration_service_deleting_action_mapping",
|
||||
mapping_id=mapping_id)
|
||||
|
||||
result = await sdk_bridge_client.delete_action_mapping(mapping_id)
|
||||
|
||||
if not result["success"]:
|
||||
logger.error("action_mapping_delete_failed", error=result.get("error_message"))
|
||||
raise ValueError(f"Action mapping deletion failed: {result.get('error_message')}")
|
||||
|
||||
logger.info("action_mapping_delete_success", mapping_id=mapping_id)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error("configuration_service_delete_action_mapping_failed", error=str(e), exc_info=True)
|
||||
raise
|
||||
|
||||
async def read_configuration_as_tree(self, max_depth: int = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Read configuration as hierarchical folder tree
|
||||
|
||||
Args:
|
||||
max_depth: Maximum depth to traverse (None = unlimited, 1 = root level only)
|
||||
|
||||
Returns:
|
||||
Dictionary with tree structure
|
||||
"""
|
||||
try:
|
||||
logger.info("configuration_service_reading_tree", max_depth=max_depth)
|
||||
result = await sdk_bridge_client.read_configuration_tree()
|
||||
|
||||
if not result["success"]:
|
||||
logger.error("configuration_tree_read_failed", error=result.get("error_message"))
|
||||
raise ValueError(f"Configuration tree read failed: {result.get('error_message')}")
|
||||
|
||||
tree = result["tree"]
|
||||
|
||||
# Apply depth limit if specified
|
||||
if max_depth is not None:
|
||||
tree = self._limit_tree_depth(tree, max_depth)
|
||||
|
||||
logger.info("configuration_tree_read_success",
|
||||
total_nodes=result["total_nodes"],
|
||||
max_depth=max_depth)
|
||||
|
||||
return tree
|
||||
|
||||
except Exception as e:
|
||||
logger.error("configuration_service_read_tree_failed", error=str(e), exc_info=True)
|
||||
raise
|
||||
|
||||
async def read_configuration_path(self, path: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Read a specific folder from configuration tree
|
||||
|
||||
Args:
|
||||
path: Path to folder (e.g., "MappingRules" or "MappingRules/1")
|
||||
|
||||
Returns:
|
||||
Dictionary with subtree
|
||||
"""
|
||||
try:
|
||||
logger.info("configuration_service_reading_path", path=path)
|
||||
result = await sdk_bridge_client.read_configuration_tree()
|
||||
|
||||
if not result["success"]:
|
||||
logger.error("configuration_tree_read_failed", error=result.get("error_message"))
|
||||
raise ValueError(f"Configuration tree read failed: {result.get('error_message')}")
|
||||
|
||||
tree = result["tree"]
|
||||
|
||||
# Navigate to requested path
|
||||
path_parts = path.split("/")
|
||||
current = tree
|
||||
|
||||
for part in path_parts:
|
||||
if not part: # Skip empty parts
|
||||
continue
|
||||
|
||||
# Find child with matching name
|
||||
if current.get("type") != "folder" or "children" not in current:
|
||||
raise ValueError(f"Path '{path}' not found: '{part}' is not a folder")
|
||||
|
||||
found = None
|
||||
for child in current["children"]:
|
||||
if child.get("name") == part:
|
||||
found = child
|
||||
break
|
||||
|
||||
if found is None:
|
||||
raise ValueError(f"Path '{path}' not found: folder '{part}' does not exist")
|
||||
|
||||
current = found
|
||||
|
||||
logger.info("configuration_path_read_success", path=path)
|
||||
return current
|
||||
|
||||
except ValueError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("configuration_service_read_path_failed", path=path, error=str(e), exc_info=True)
|
||||
raise
|
||||
|
||||
def _limit_tree_depth(self, node: Dict[str, Any], max_depth: int, current_depth: int = 0) -> Dict[str, Any]:
|
||||
"""
|
||||
Limit tree depth by removing children beyond max_depth
|
||||
|
||||
Args:
|
||||
node: Tree node
|
||||
max_depth: Maximum depth
|
||||
current_depth: Current depth (internal)
|
||||
|
||||
Returns:
|
||||
Tree node with limited depth
|
||||
"""
|
||||
if current_depth >= max_depth:
|
||||
# At max depth - remove children
|
||||
limited = {k: v for k, v in node.items() if k != "children"}
|
||||
return limited
|
||||
|
||||
# Not at max depth yet - recurse into children
|
||||
result = node.copy()
|
||||
if "children" in node and node.get("type") == "folder":
|
||||
result["children"] = [
|
||||
self._limit_tree_depth(child, max_depth, current_depth + 1)
|
||||
for child in node["children"]
|
||||
]
|
||||
|
||||
return result
|
||||
|
||||
async def create_server(self, server_data: dict) -> dict:
|
||||
"""
|
||||
Create a new G-core server and persist to GeViServer
|
||||
|
||||
Args:
|
||||
server_data: Dictionary with server configuration (id, alias, host, user, password, enabled, etc.)
|
||||
|
||||
Returns:
|
||||
Dictionary with success status and created server
|
||||
"""
|
||||
try:
|
||||
server_id = server_data.get("id")
|
||||
if not server_id:
|
||||
raise ValueError("Server ID is required")
|
||||
|
||||
logger.info("configuration_service_creating_server", server_id=server_id)
|
||||
|
||||
# Read current tree
|
||||
tree_result = await sdk_bridge_client.read_configuration_tree()
|
||||
if not tree_result["success"]:
|
||||
raise ValueError(f"Failed to read configuration tree: {tree_result.get('error_message')}")
|
||||
|
||||
tree = tree_result["tree"]
|
||||
|
||||
# Find GeViGCoreServer folder
|
||||
gcore_folder = self._find_child(tree, "GeViGCoreServer")
|
||||
if not gcore_folder:
|
||||
raise ValueError("GeViGCoreServer folder not found in configuration")
|
||||
|
||||
# Check if server already exists
|
||||
if self._find_child(gcore_folder, server_id):
|
||||
raise ValueError(f"Server '{server_id}' already exists")
|
||||
|
||||
# Create new server folder structure
|
||||
new_server = {
|
||||
"type": "folder",
|
||||
"name": server_id,
|
||||
"children": [
|
||||
{"type": "string", "name": "Alias", "value": server_data.get("alias", "")},
|
||||
{"type": "string", "name": "Host", "value": server_data.get("host", "")},
|
||||
{"type": "string", "name": "User", "value": server_data.get("user", "")},
|
||||
{"type": "string", "name": "Password", "value": server_data.get("password", "")},
|
||||
{"type": "int32", "name": "Enabled", "value": 1 if server_data.get("enabled", True) else 0},
|
||||
{"type": "int32", "name": "DeactivateEcho", "value": 1 if server_data.get("deactivateEcho", False) else 0},
|
||||
{"type": "int32", "name": "DeactivateLiveCheck", "value": 1 if server_data.get("deactivateLiveCheck", False) else 0}
|
||||
]
|
||||
}
|
||||
|
||||
# Add server to GeViGCoreServer folder
|
||||
if "children" not in gcore_folder:
|
||||
gcore_folder["children"] = []
|
||||
gcore_folder["children"].append(new_server)
|
||||
|
||||
# Write modified tree back to GeViServer
|
||||
write_result = await sdk_bridge_client.write_configuration_tree(tree)
|
||||
|
||||
if not write_result["success"]:
|
||||
raise ValueError(f"Failed to write configuration: {write_result.get('error_message')}")
|
||||
|
||||
logger.info("configuration_service_server_created", server_id=server_id,
|
||||
bytes_written=write_result.get("bytes_written"))
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Server '{server_id}' created successfully",
|
||||
"server": server_data,
|
||||
"bytes_written": write_result.get("bytes_written")
|
||||
}
|
||||
|
||||
except ValueError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("configuration_service_create_server_failed", error=str(e), exc_info=True)
|
||||
raise
|
||||
|
||||
async def update_server(self, server_id: str, server_data: dict) -> dict:
|
||||
"""
|
||||
Update an existing G-core server and persist to GeViServer
|
||||
|
||||
Args:
|
||||
server_id: ID of the server to update
|
||||
server_data: Dictionary with updated server configuration
|
||||
|
||||
Returns:
|
||||
Dictionary with success status
|
||||
"""
|
||||
try:
|
||||
logger.info("configuration_service_updating_server", server_id=server_id)
|
||||
|
||||
# Read current tree
|
||||
tree_result = await sdk_bridge_client.read_configuration_tree()
|
||||
if not tree_result["success"]:
|
||||
raise ValueError(f"Failed to read configuration tree: {tree_result.get('error_message')}")
|
||||
|
||||
tree = tree_result["tree"]
|
||||
|
||||
# Find GeViGCoreServer folder
|
||||
gcore_folder = self._find_child(tree, "GeViGCoreServer")
|
||||
if not gcore_folder:
|
||||
raise ValueError("GeViGCoreServer folder not found in configuration")
|
||||
|
||||
# Find the server to update
|
||||
server_folder = self._find_child(gcore_folder, server_id)
|
||||
if not server_folder:
|
||||
raise ValueError(f"Server '{server_id}' not found")
|
||||
|
||||
# Update server properties
|
||||
children_dict = {c.get("name"): c for c in server_folder.get("children", [])}
|
||||
|
||||
if "alias" in server_data:
|
||||
if "Alias" in children_dict:
|
||||
children_dict["Alias"]["value"] = server_data["alias"]
|
||||
else:
|
||||
server_folder.setdefault("children", []).append(
|
||||
{"type": "string", "name": "Alias", "value": server_data["alias"]}
|
||||
)
|
||||
|
||||
if "host" in server_data:
|
||||
if "Host" in children_dict:
|
||||
children_dict["Host"]["value"] = server_data["host"]
|
||||
else:
|
||||
server_folder.setdefault("children", []).append(
|
||||
{"type": "string", "name": "Host", "value": server_data["host"]}
|
||||
)
|
||||
|
||||
if "user" in server_data:
|
||||
if "User" in children_dict:
|
||||
children_dict["User"]["value"] = server_data["user"]
|
||||
else:
|
||||
server_folder.setdefault("children", []).append(
|
||||
{"type": "string", "name": "User", "value": server_data["user"]}
|
||||
)
|
||||
|
||||
if "password" in server_data:
|
||||
if "Password" in children_dict:
|
||||
children_dict["Password"]["value"] = server_data["password"]
|
||||
else:
|
||||
server_folder.setdefault("children", []).append(
|
||||
{"type": "string", "name": "Password", "value": server_data["password"]}
|
||||
)
|
||||
|
||||
if "enabled" in server_data:
|
||||
enabled_value = 1 if server_data["enabled"] else 0
|
||||
if "Enabled" in children_dict:
|
||||
children_dict["Enabled"]["value"] = enabled_value
|
||||
else:
|
||||
server_folder.setdefault("children", []).append(
|
||||
{"type": "int32", "name": "Enabled", "value": enabled_value}
|
||||
)
|
||||
|
||||
if "deactivateEcho" in server_data:
|
||||
echo_value = 1 if server_data["deactivateEcho"] else 0
|
||||
if "DeactivateEcho" in children_dict:
|
||||
children_dict["DeactivateEcho"]["value"] = echo_value
|
||||
else:
|
||||
server_folder.setdefault("children", []).append(
|
||||
{"type": "int32", "name": "DeactivateEcho", "value": echo_value}
|
||||
)
|
||||
|
||||
if "deactivateLiveCheck" in server_data:
|
||||
livecheck_value = 1 if server_data["deactivateLiveCheck"] else 0
|
||||
if "DeactivateLiveCheck" in children_dict:
|
||||
children_dict["DeactivateLiveCheck"]["value"] = livecheck_value
|
||||
else:
|
||||
server_folder.setdefault("children", []).append(
|
||||
{"type": "int32", "name": "DeactivateLiveCheck", "value": livecheck_value}
|
||||
)
|
||||
|
||||
# Write modified tree back to GeViServer
|
||||
write_result = await sdk_bridge_client.write_configuration_tree(tree)
|
||||
|
||||
if not write_result["success"]:
|
||||
raise ValueError(f"Failed to write configuration: {write_result.get('error_message')}")
|
||||
|
||||
logger.info("configuration_service_server_updated", server_id=server_id,
|
||||
bytes_written=write_result.get("bytes_written"))
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Server '{server_id}' updated successfully",
|
||||
"bytes_written": write_result.get("bytes_written")
|
||||
}
|
||||
|
||||
except ValueError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("configuration_service_update_server_failed", server_id=server_id, error=str(e), exc_info=True)
|
||||
raise
|
||||
|
||||
async def delete_server(self, server_id: str) -> dict:
|
||||
"""
|
||||
Delete a G-core server and persist to GeViServer
|
||||
|
||||
Args:
|
||||
server_id: ID of the server to delete
|
||||
|
||||
Returns:
|
||||
Dictionary with success status
|
||||
"""
|
||||
try:
|
||||
logger.info("configuration_service_deleting_server", server_id=server_id)
|
||||
|
||||
# Read current tree
|
||||
tree_result = await sdk_bridge_client.read_configuration_tree()
|
||||
if not tree_result["success"]:
|
||||
raise ValueError(f"Failed to read configuration tree: {tree_result.get('error_message')}")
|
||||
|
||||
tree = tree_result["tree"]
|
||||
|
||||
# Find GeViGCoreServer folder
|
||||
gcore_folder = self._find_child(tree, "GeViGCoreServer")
|
||||
if not gcore_folder:
|
||||
raise ValueError("GeViGCoreServer folder not found in configuration")
|
||||
|
||||
# Find and remove the server
|
||||
if "children" not in gcore_folder:
|
||||
raise ValueError(f"Server '{server_id}' not found")
|
||||
|
||||
server_index = None
|
||||
for i, child in enumerate(gcore_folder["children"]):
|
||||
if child.get("name") == server_id and child.get("type") == "folder":
|
||||
server_index = i
|
||||
break
|
||||
|
||||
if server_index is None:
|
||||
raise ValueError(f"Server '{server_id}' not found")
|
||||
|
||||
# Remove server from children list
|
||||
gcore_folder["children"].pop(server_index)
|
||||
|
||||
# Write modified tree back to GeViServer
|
||||
write_result = await sdk_bridge_client.write_configuration_tree(tree)
|
||||
|
||||
if not write_result["success"]:
|
||||
raise ValueError(f"Failed to write configuration: {write_result.get('error_message')}")
|
||||
|
||||
logger.info("configuration_service_server_deleted", server_id=server_id,
|
||||
bytes_written=write_result.get("bytes_written"))
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Server '{server_id}' deleted successfully",
|
||||
"bytes_written": write_result.get("bytes_written")
|
||||
}
|
||||
|
||||
except ValueError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("configuration_service_delete_server_failed", server_id=server_id, error=str(e), exc_info=True)
|
||||
raise
|
||||
|
||||
def _find_child(self, parent: dict, child_name: str) -> dict:
|
||||
"""
|
||||
Helper method to find a child node by name
|
||||
|
||||
Args:
|
||||
parent: Parent node (folder)
|
||||
child_name: Name of child to find
|
||||
|
||||
Returns:
|
||||
Child node or None if not found
|
||||
"""
|
||||
if parent.get("type") != "folder" or "children" not in parent:
|
||||
return None
|
||||
|
||||
for child in parent["children"]:
|
||||
if child.get("name") == child_name:
|
||||
return child
|
||||
|
||||
return None
|
||||
Reference in New Issue
Block a user