""" Infrastructure service layer for business logic and database operations. This module handles all database operations for infrastructure assets, providing a clean separation between the API routes and data access layer. """ from typing import Optional from uuid import UUID from fastapi import HTTPException, status from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session from api.models.infrastructure import Infrastructure from api.schemas.infrastructure import InfrastructureCreate, InfrastructureUpdate def get_infrastructure_items(db: Session, skip: int = 0, limit: int = 100) -> tuple[list[Infrastructure], int]: """ Retrieve a paginated list of infrastructure items. Args: db: Database session skip: Number of records to skip (for pagination) limit: Maximum number of records to return Returns: tuple: (list of infrastructure items, total count) Example: ```python items, total = get_infrastructure_items(db, skip=0, limit=50) print(f"Retrieved {len(items)} of {total} infrastructure items") ``` """ # Get total count total = db.query(Infrastructure).count() # Get paginated results, ordered by created_at descending (newest first) items = ( db.query(Infrastructure) .order_by(Infrastructure.created_at.desc()) .offset(skip) .limit(limit) .all() ) return items, total def get_infrastructure_by_id(db: Session, infrastructure_id: UUID) -> Infrastructure: """ Retrieve a single infrastructure item by its ID. Args: db: Database session infrastructure_id: UUID of the infrastructure item to retrieve Returns: Infrastructure: The infrastructure object Raises: HTTPException: 404 if infrastructure not found Example: ```python item = get_infrastructure_by_id(db, infrastructure_id) print(f"Found infrastructure: {item.hostname}") ``` """ item = db.query(Infrastructure).filter(Infrastructure.id == str(infrastructure_id)).first() if not item: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Infrastructure with ID {infrastructure_id} not found" ) return item def get_infrastructure_by_site(db: Session, site_id: str, skip: int = 0, limit: int = 100) -> tuple[list[Infrastructure], int]: """ Retrieve infrastructure items for a specific site. Args: db: Database session site_id: UUID of the site skip: Number of records to skip (for pagination) limit: Maximum number of records to return Returns: tuple: (list of infrastructure items, total count) Example: ```python items, total = get_infrastructure_by_site(db, site_id, skip=0, limit=50) print(f"Retrieved {len(items)} of {total} items for site") ``` """ # Get total count for this site total = db.query(Infrastructure).filter(Infrastructure.site_id == site_id).count() # Get paginated results items = ( db.query(Infrastructure) .filter(Infrastructure.site_id == site_id) .order_by(Infrastructure.created_at.desc()) .offset(skip) .limit(limit) .all() ) return items, total def get_infrastructure_by_client(db: Session, client_id: str, skip: int = 0, limit: int = 100) -> tuple[list[Infrastructure], int]: """ Retrieve infrastructure items for a specific client. Args: db: Database session client_id: UUID of the client skip: Number of records to skip (for pagination) limit: Maximum number of records to return Returns: tuple: (list of infrastructure items, total count) Example: ```python items, total = get_infrastructure_by_client(db, client_id, skip=0, limit=50) print(f"Retrieved {len(items)} of {total} items for client") ``` """ # Get total count for this client total = db.query(Infrastructure).filter(Infrastructure.client_id == client_id).count() # Get paginated results items = ( db.query(Infrastructure) .filter(Infrastructure.client_id == client_id) .order_by(Infrastructure.created_at.desc()) .offset(skip) .limit(limit) .all() ) return items, total def get_infrastructure_by_type(db: Session, infra_type: str, skip: int = 0, limit: int = 100) -> tuple[list[Infrastructure], int]: """ Retrieve infrastructure items by asset type. Args: db: Database session infra_type: Asset type to filter by skip: Number of records to skip (for pagination) limit: Maximum number of records to return Returns: tuple: (list of infrastructure items, total count) Example: ```python items, total = get_infrastructure_by_type(db, "physical_server", skip=0, limit=50) print(f"Retrieved {len(items)} of {total} physical servers") ``` """ # Get total count for this type total = db.query(Infrastructure).filter(Infrastructure.asset_type == infra_type).count() # Get paginated results items = ( db.query(Infrastructure) .filter(Infrastructure.asset_type == infra_type) .order_by(Infrastructure.created_at.desc()) .offset(skip) .limit(limit) .all() ) return items, total def create_infrastructure(db: Session, infrastructure_data: InfrastructureCreate) -> Infrastructure: """ Create a new infrastructure item. Args: db: Database session infrastructure_data: Infrastructure creation data Returns: Infrastructure: The created infrastructure object Raises: HTTPException: 409 if validation fails HTTPException: 422 if foreign key validation fails HTTPException: 500 if database error occurs Example: ```python infra_data = InfrastructureCreate( hostname="server-01", asset_type="physical_server", client_id="client-uuid" ) infra = create_infrastructure(db, infra_data) print(f"Created infrastructure: {infra.id}") ``` """ # Validate foreign keys if provided if infrastructure_data.client_id: from api.models.client import Client client = db.query(Client).filter(Client.id == infrastructure_data.client_id).first() if not client: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=f"Client with ID {infrastructure_data.client_id} not found" ) if infrastructure_data.site_id: from api.models.site import Site site = db.query(Site).filter(Site.id == infrastructure_data.site_id).first() if not site: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=f"Site with ID {infrastructure_data.site_id} not found" ) if infrastructure_data.parent_host_id: parent = db.query(Infrastructure).filter(Infrastructure.id == infrastructure_data.parent_host_id).first() if not parent: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=f"Parent host with ID {infrastructure_data.parent_host_id} not found" ) try: # Create new infrastructure instance db_infrastructure = Infrastructure(**infrastructure_data.model_dump()) # Add to database db.add(db_infrastructure) db.commit() db.refresh(db_infrastructure) return db_infrastructure except IntegrityError as e: db.rollback() # Handle constraint violations if "client_id" in str(e.orig): raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=f"Invalid client_id: {infrastructure_data.client_id}" ) elif "site_id" in str(e.orig): raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=f"Invalid site_id: {infrastructure_data.site_id}" ) elif "parent_host_id" in str(e.orig): raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=f"Invalid parent_host_id: {infrastructure_data.parent_host_id}" ) else: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Database error: {str(e)}" ) except Exception as e: db.rollback() raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to create infrastructure: {str(e)}" ) def update_infrastructure(db: Session, infrastructure_id: UUID, infrastructure_data: InfrastructureUpdate) -> Infrastructure: """ Update an existing infrastructure item. Args: db: Database session infrastructure_id: UUID of the infrastructure item to update infrastructure_data: Infrastructure update data (only provided fields will be updated) Returns: Infrastructure: The updated infrastructure object Raises: HTTPException: 404 if infrastructure not found HTTPException: 422 if foreign key validation fails HTTPException: 500 if database error occurs Example: ```python update_data = InfrastructureUpdate( status="decommissioned", notes="Server retired" ) infra = update_infrastructure(db, infrastructure_id, update_data) print(f"Updated infrastructure: {infra.hostname}") ``` """ # Get existing infrastructure infrastructure = get_infrastructure_by_id(db, infrastructure_id) try: # Update only provided fields update_data = infrastructure_data.model_dump(exclude_unset=True) # Validate foreign keys if being updated if "client_id" in update_data and update_data["client_id"]: from api.models.client import Client client = db.query(Client).filter(Client.id == update_data["client_id"]).first() if not client: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=f"Client with ID {update_data['client_id']} not found" ) if "site_id" in update_data and update_data["site_id"]: from api.models.site import Site site = db.query(Site).filter(Site.id == update_data["site_id"]).first() if not site: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=f"Site with ID {update_data['site_id']} not found" ) if "parent_host_id" in update_data and update_data["parent_host_id"]: parent = db.query(Infrastructure).filter(Infrastructure.id == update_data["parent_host_id"]).first() if not parent: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=f"Parent host with ID {update_data['parent_host_id']} not found" ) # Apply updates for field, value in update_data.items(): setattr(infrastructure, field, value) db.commit() db.refresh(infrastructure) return infrastructure except HTTPException: db.rollback() raise except IntegrityError as e: db.rollback() if "client_id" in str(e.orig): raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Invalid client_id" ) elif "site_id" in str(e.orig): raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Invalid site_id" ) elif "parent_host_id" in str(e.orig): raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Invalid parent_host_id" ) else: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Database error: {str(e)}" ) except Exception as e: db.rollback() raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to update infrastructure: {str(e)}" ) def delete_infrastructure(db: Session, infrastructure_id: UUID) -> dict: """ Delete an infrastructure item by its ID. Args: db: Database session infrastructure_id: UUID of the infrastructure item to delete Returns: dict: Success message Raises: HTTPException: 404 if infrastructure not found HTTPException: 500 if database error occurs Example: ```python result = delete_infrastructure(db, infrastructure_id) print(result["message"]) # "Infrastructure deleted successfully" ``` """ # Get existing infrastructure (raises 404 if not found) infrastructure = get_infrastructure_by_id(db, infrastructure_id) try: db.delete(infrastructure) db.commit() return { "message": "Infrastructure deleted successfully", "infrastructure_id": str(infrastructure_id) } except Exception as e: db.rollback() raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to delete infrastructure: {str(e)}" )