178 lines
5.7 KiB
Python
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
|
|
|
|
|