import uuid from sqlalchemy import select, insert, bindparam, and_ from sqlalchemy.orm import Session from starlette.exceptions import HTTPException from starlette.requests import Request from fastapi import APIRouter from starlette.responses import JSONResponse from pillar_tool.db import Host from pillar_tool.db.queries.host_queries import create_host from pillar_tool.schemas import HostCreateParams from pillar_tool.util.validation import validate_fqdn, split_and_validate_path router = APIRouter( prefix="/host", tags=["Host"], ) # TODO: check comments in this file (they are written by AI) @router.get("") def hosts_get(req: Request): """ Retrieve a list of all hosts (excluding hostgroups) in the system. Queries the database for all host entries where is_hostgroup is False, returning only the names of the hosts in a flat list format. Args: req: FastAPI request object containing database session Returns: JSONResponse containing a list of host names as strings """ db: Session = req.state.db result = db.execute(select(Host).where(Host.is_hostgroup == False)).fetchall() hosts: list[Host] = list(map(lambda x: x[0], result)) return JSONResponse(status_code=200, content=list(map(lambda x: x.name, hosts))) @router.get("/{fqdn}") def host_get(req: Request, fqdn: str): """ Retrieve detailed information about a specific host including its hierarchical path. Fetches host details from the database and constructs the full path by traversing parent relationships up to the root. Returns both the host name and its complete hierarchical path as a slash-separated string. Args: req: FastAPI request object containing database session fqdn: Fully qualified domain name of the host to retrieve Returns: JSONResponse containing host name and hierarchical path Raises: HTTPException: If FQDN format is invalid or host is not found """ db: Session = req.state.db if not validate_fqdn(fqdn): raise HTTPException(status_code=400, detail="Provided host is not an FQDN") host_stmt = select(Host).where(Host.name == fqdn) result = db.execute(host_stmt).fetchall() if len(result) != 1: raise HTTPException(status_code=404, detail=f"No such host found (length of result was {len(result)})") host: Host = result[0][0] last_parent = host path = [] parent_stmt = select(Host).where(Host.id == bindparam('parent_id')) while host.parent_id is not None: result = db.execute(parent_stmt, { 'parent_id': last_parent.parent_id }).fetchall() # Note: this assertion should be enforced by the database assert len(result) == 1 parent = result[0][0] path.append(parent) last_parent = parent path.reverse() return JSONResponse(status_code=200, content={ "host": host.name, "path": '/'.join(map(lambda x: x.name, path)) }) @router.post("/{fqdn}") async def host_add(request: Request, fqdn: str, params: HostCreateParams): """ Create a new host with optional parent hierarchy. Validates FQDN format and parent path structure before creating the host. If a parent is specified, ensures all parent components exist in the database. Creates the host with a unique UUID and proper hierarchical relationships. Args: request: FastAPI request object containing database session fqdn: Fully qualified domain name for the new host params: Host creation parameters including optional parent path Returns: JSONResponse with creation details and host information Raises: HTTPException: If FQDN format is invalid or parent doesn't exist """ db: Session = request.state.db # Validate that the provided FQDN is properly formatted if not validate_fqdn(fqdn): raise HTTPException(status_code=400, detail="Provided host is not an FQDN") # Process parent path if provided if params.parent is not None: parent_labels = split_and_validate_path(params.parent) if parent_labels is None: raise HTTPException(status_code=400, detail="Provided parent is not a valid path") else: parent_labels = [] # Traverse the parent hierarchy to ensure all components exist parent_id = None stmt_select_respecting_parent = select(Host).where(and_(Host.name == bindparam("label"), Host.parent_id == bindparam("parent_id"))) for label in parent_labels: result = db.execute(stmt_select_respecting_parent, { 'label': label, 'parent_id': parent_id }).fetchall() if len(result) == 0: raise HTTPException(status_code=400, detail="Parent does not exist") # Note: this should be enforced by the database assert len(result) == 1 parent_id = result[0][0].parent_id # Create new host with unique ID and hierarchical structure new_host = Host( id=uuid.uuid4(), name=fqdn, parent_id=parent_id, is_hostgroup=False ) stmt_create_host_with_parent = insert(Host).values(id=new_host.id, name=new_host.name, parent_id=new_host.parent_id, is_hostgroup=new_host.is_hostgroup) db.execute(stmt_create_host_with_parent).fetchall() # Prepare response with creation details output = { "message": "Host created", "host": new_host, # return the final host in the hierarchy } # include the full path to the new host if it exists if params.parent is not None: output.update({ "path": params.parent }) return JSONResponse(output) @router.delete("/{fqdn}") async def host_delete(request: Request, fqdn: str): """ Delete a specific host from the system by its FQDN. Removes the host entry and associated data from the database. TODO: Implement actual deletion logic - currently just a stub. Args: request: FastAPI request object containing database session fqdn: Fully qualified domain name of the host to delete Returns: JSONResponse indicating success or failure of the operation Raises: HTTPException: If FQDN format is invalid or host is not found """ pass