utp_service/backend/app/routers/objects.py
2026-04-08 00:20:16 +03:00

208 lines
6.7 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select, distinct
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.dependencies import get_current_user, get_db
from app.models.device import Device
from app.models.object import Object
from app.models.tag import Tag
from app.models.user import User
from app.schemas.admin import SheetsSyncRequest
from app.schemas.object import ObjectCreate, ObjectMetaResponse, ObjectRead, ObjectUpdate, SyncResult
from app.services import crypto
from app.services.device_discovery import discover_via_ssh
from app.services.object_db import sync_from_object_db
from app.services.sheets import sync_from_sheets
router = APIRouter(prefix="/api/v1/objects", tags=["objects"])
def _apply_credentials(obj: Object, db_password: str | None, ssh_password: str | None) -> None:
if db_password is not None:
obj.db_pass_enc = crypto.encrypt(db_password)
if ssh_password is not None:
obj.ssh_pass_enc = crypto.encrypt(ssh_password)
async def _load_object(db: AsyncSession, obj_id: int) -> Object:
result = await db.execute(
select(Object).options(selectinload(Object.tags)).where(Object.id == obj_id)
)
obj = result.scalar_one_or_none()
if obj is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Object not found")
return obj
@router.get("/meta", response_model=ObjectMetaResponse)
async def get_objects_meta(
db: AsyncSession = Depends(get_db),
_: User = Depends(get_current_user),
):
"""Return distinct city and client values for autocomplete."""
cities_result = await db.execute(
select(distinct(Object.city)).where(Object.city.isnot(None)).order_by(Object.city)
)
clients_result = await db.execute(
select(distinct(Object.client)).where(Object.client.isnot(None)).order_by(Object.client)
)
return ObjectMetaResponse(
cities=[r for (r,) in cities_result.all()],
clients=[r for (r,) in clients_result.all()],
)
@router.get("", response_model=list[ObjectRead])
async def list_objects(
is_active: bool | None = None,
tag_id: int | None = None,
city: str | None = None,
client: str | None = None,
db: AsyncSession = Depends(get_db),
_: User = Depends(get_current_user),
):
query = select(Object).options(selectinload(Object.tags))
if is_active is not None:
query = query.where(Object.is_active == is_active)
if tag_id is not None:
query = query.where(Object.tags.any(Tag.id == tag_id))
if city is not None:
query = query.where(Object.city == city)
if client is not None:
query = query.where(Object.client == client)
query = query.order_by(Object.city, Object.client, Object.name)
result = await db.execute(query)
return result.scalars().all()
@router.post("", response_model=ObjectRead, status_code=status.HTTP_201_CREATED)
async def create_object(
body: ObjectCreate,
db: AsyncSession = Depends(get_db),
_: User = Depends(get_current_user),
):
data = body.model_dump(exclude={"db_password", "ssh_password", "tag_ids"})
obj = Object(**data)
_apply_credentials(obj, body.db_password, body.ssh_password)
if body.tag_ids:
tags_result = await db.execute(select(Tag).where(Tag.id.in_(body.tag_ids)))
obj.tags = list(tags_result.scalars().all())
db.add(obj)
await db.flush()
await db.refresh(obj, ["tags"])
return obj
@router.get("/{obj_id}", response_model=ObjectRead)
async def get_object(
obj_id: int,
db: AsyncSession = Depends(get_db),
_: User = Depends(get_current_user),
):
return await _load_object(db, obj_id)
@router.patch("/{obj_id}", response_model=ObjectRead)
async def update_object(
obj_id: int,
body: ObjectUpdate,
db: AsyncSession = Depends(get_db),
_: User = Depends(get_current_user),
):
obj = await _load_object(db, obj_id)
data = body.model_dump(exclude_none=True, exclude={"db_password", "ssh_password", "tag_ids"})
for field, value in data.items():
setattr(obj, field, value)
_apply_credentials(obj, body.db_password, body.ssh_password)
if body.tag_ids is not None:
tags_result = await db.execute(select(Tag).where(Tag.id.in_(body.tag_ids)))
obj.tags = list(tags_result.scalars().all())
await db.flush()
await db.refresh(obj)
return obj
@router.delete("/{obj_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_object(
obj_id: int,
db: AsyncSession = Depends(get_db),
_: User = Depends(get_current_user),
):
obj = await _load_object(db, obj_id)
# Check for devices (RESTRICT constraint would catch this, but give a clear message)
count_result = await db.execute(
select(Device.id).where(Device.object_id == obj_id).limit(1)
)
if count_result.scalar_one_or_none() is not None:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Cannot delete object with existing devices. Remove all devices first.",
)
await db.delete(obj)
@router.post("/{obj_id}/sync", response_model=SyncResult)
async def sync_devices(
obj_id: int,
db: AsyncSession = Depends(get_db),
_: User = Depends(get_current_user),
):
obj = await _load_object(db, obj_id)
result = await sync_from_object_db(db, obj)
return SyncResult(
created=result.created,
updated=result.updated,
skipped=result.skipped,
errors=result.errors,
)
@router.post("/{obj_id}/discover", response_model=SyncResult)
async def discover_devices(
obj_id: int,
db: AsyncSession = Depends(get_db),
_: User = Depends(get_current_user),
):
"""SSH into each rack PC, parse caps config, discover network devices."""
obj = await _load_object(db, obj_id)
result = await discover_via_ssh(db, obj)
await db.flush()
return SyncResult(
created=result.created,
updated=result.updated,
skipped=result.skipped,
errors=result.errors,
)
@router.post("/{obj_id}/sync/sheets", response_model=SyncResult)
async def sync_devices_from_sheets(
obj_id: int,
body: SheetsSyncRequest,
db: AsyncSession = Depends(get_db),
_: User = Depends(get_current_user),
):
await _load_object(db, obj_id)
try:
result = await sync_from_sheets(
db,
object_id=obj_id,
spreadsheet_id=body.spreadsheet_id,
sheet_name=body.sheet_name,
)
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc))
return SyncResult(
created=result.created,
updated=result.updated,
skipped=result.skipped,
errors=result.errors,
)