123 lines
5.7 KiB
Python
123 lines
5.7 KiB
Python
"""总控平台设备分组 CRUD 与成员变更接口。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
from typing import Optional
|
|
|
|
from fastapi import APIRouter, Header
|
|
from fastapi.responses import JSONResponse
|
|
|
|
from models.workbench import DeviceGroupCreate, DeviceGroupMembers, DeviceGroupPatch
|
|
from services.device_groups import device_group_service
|
|
from services.workbench import ControlPlaneStorageUnavailable, IdempotencyConflict, error_body
|
|
|
|
router = APIRouter(prefix="/device-groups", tags=["设备分组"])
|
|
|
|
|
|
def _write_meta(key: Optional[str], actor: Optional[str]) -> tuple[Optional[str], str, str]:
|
|
return key.strip() if key else None, (actor or "system").strip() or "system", uuid.uuid4().hex
|
|
|
|
|
|
def _error(status: int, code: str, message: str, trace_id: Optional[str] = None):
|
|
return JSONResponse(status_code=status, content=error_body(code, message, trace_id))
|
|
|
|
|
|
def _storage_error(exc: Exception):
|
|
return _error(503, "control_plane_storage_unavailable", str(exc))
|
|
|
|
|
|
def _read_only_empty(exc: Exception):
|
|
"""仓库暂时没接上时,列表页返回可识别的空结果,不把整页打成 500。"""
|
|
return {
|
|
"code": 200,
|
|
"success": True,
|
|
"data": {
|
|
"items": [],
|
|
"data_state": "storage_unavailable",
|
|
"source": "control_plane_storage",
|
|
"message": str(exc),
|
|
},
|
|
}
|
|
|
|
|
|
@router.get("", response_model=dict)
|
|
async def list_device_groups():
|
|
try:
|
|
return {"code": 200, "success": True, "data": {"items": await device_group_service.list_groups()}}
|
|
except ControlPlaneStorageUnavailable as exc:
|
|
return _read_only_empty(exc)
|
|
|
|
|
|
@router.get("/{group_id}", response_model=dict)
|
|
async def get_device_group(group_id: str):
|
|
try:
|
|
item = await device_group_service.get_group(group_id)
|
|
except ControlPlaneStorageUnavailable as exc:
|
|
return _storage_error(exc)
|
|
if item is None:
|
|
return _error(404, "resource_not_found", "设备分组不存在")
|
|
return {"code": 200, "success": True, "data": item}
|
|
|
|
|
|
@router.post("", response_model=dict)
|
|
async def create_device_group(body: DeviceGroupCreate, idempotency_key: Optional[str] = Header(None, alias="Idempotency-Key"), actor: Optional[str] = Header(None, alias="X-Actor-Id")):
|
|
key, actor_id, trace_id = _write_meta(idempotency_key, actor)
|
|
if not key:
|
|
return _error(400, "validation_error", "所有写操作都需要 Idempotency-Key", trace_id)
|
|
try:
|
|
result = await device_group_service.create(name=body.name, device_ids=body.device_ids, description=body.description, actor=actor_id, key=key, trace_id=trace_id)
|
|
return {"code": 200, "success": True, "data": result}
|
|
except IdempotencyConflict:
|
|
return _error(409, "idempotency_conflict", "同一幂等键对应了不同请求", trace_id)
|
|
except ControlPlaneStorageUnavailable as exc:
|
|
return _storage_error(exc)
|
|
|
|
|
|
@router.patch("/{group_id}", response_model=dict)
|
|
async def patch_device_group(group_id: str, body: DeviceGroupPatch, idempotency_key: Optional[str] = Header(None, alias="Idempotency-Key"), actor: Optional[str] = Header(None, alias="X-Actor-Id")):
|
|
key, actor_id, trace_id = _write_meta(idempotency_key, actor)
|
|
if not key:
|
|
return _error(400, "validation_error", "所有写操作都需要 Idempotency-Key", trace_id)
|
|
try:
|
|
result = await device_group_service.patch(group_id=group_id, name=body.name, description=body.description, actor=actor_id, key=key, trace_id=trace_id)
|
|
if result is None:
|
|
return _error(404, "resource_not_found", "设备分组不存在", trace_id)
|
|
return {"code": 200, "success": True, "data": result}
|
|
except IdempotencyConflict:
|
|
return _error(409, "idempotency_conflict", "同一幂等键对应了不同请求", trace_id)
|
|
except ControlPlaneStorageUnavailable as exc:
|
|
return _storage_error(exc)
|
|
|
|
|
|
@router.post("/{group_id}/members", response_model=dict)
|
|
async def mutate_device_group_members(group_id: str, body: DeviceGroupMembers, idempotency_key: Optional[str] = Header(None, alias="Idempotency-Key"), actor: Optional[str] = Header(None, alias="X-Actor-Id")):
|
|
key, actor_id, trace_id = _write_meta(idempotency_key, actor)
|
|
if not key:
|
|
return _error(400, "validation_error", "所有写操作都需要 Idempotency-Key", trace_id)
|
|
try:
|
|
result = await device_group_service.mutate_members(group_id=group_id, device_ids=body.device_ids, mode=body.mode, actor=actor_id, key=key, trace_id=trace_id)
|
|
if result is None:
|
|
return _error(404, "resource_not_found", "设备分组不存在", trace_id)
|
|
return {"code": 200, "success": True, "data": result}
|
|
except IdempotencyConflict:
|
|
return _error(409, "idempotency_conflict", "同一幂等键对应了不同请求", trace_id)
|
|
except ControlPlaneStorageUnavailable as exc:
|
|
return _storage_error(exc)
|
|
|
|
|
|
@router.delete("/{group_id}", response_model=dict)
|
|
async def delete_device_group(group_id: str, idempotency_key: Optional[str] = Header(None, alias="Idempotency-Key"), actor: Optional[str] = Header(None, alias="X-Actor-Id")):
|
|
key, actor_id, trace_id = _write_meta(idempotency_key, actor)
|
|
if not key:
|
|
return _error(400, "validation_error", "所有写操作都需要 Idempotency-Key", trace_id)
|
|
try:
|
|
result = await device_group_service.delete(group_id=group_id, actor=actor_id, key=key, trace_id=trace_id)
|
|
if result is None:
|
|
return _error(404, "resource_not_found", "设备分组不存在", trace_id)
|
|
return {"code": 200, "success": True, "data": result}
|
|
except IdempotencyConflict:
|
|
return _error(409, "idempotency_conflict", "同一幂等键对应了不同请求", trace_id)
|
|
except ControlPlaneStorageUnavailable as exc:
|
|
return _storage_error(exc)
|