178 lines
5.7 KiB
Python

import uuid
from sqlalchemy import select, insert, bindparam
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(Host.name == bindparam("label") and 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):
pass