diff --git a/pillar_tool/main.py b/pillar_tool/main.py index d6aaeff..f12b4f7 100644 --- a/pillar_tool/main.py +++ b/pillar_tool/main.py @@ -1,4 +1,6 @@ -from pillar_tool.schemas import HealthCheckError +from sqlalchemy import text + +from pillar_tool.schemas import HealthCheckError, HealthCheckSuccess from pillar_tool.util import load_config, config load_config() @@ -22,6 +24,7 @@ from pillar_tool.db.database import run_db_migrations # import all the routers from pillar_tool.routers.host import router as host_router +from pillar_tool.routers.hostgroup import router as hostgroup_router # run any pending migrations run_db_migrations() @@ -66,6 +69,7 @@ app.exception_handler(Exception)(on_general_error) # Setup the api router app.include_router(host_router) +app.include_router(hostgroup_router) @app.get("/") async def root(): @@ -77,7 +81,7 @@ async def health(): # Check database connection try: db = get_connection() - db.execute("SELECT 1") + db.execute(text("SELECT 1")) db.close() except Exception as e: return HealthCheckError(500, f"Database connection error:\n{e}").response() diff --git a/pillar_tool/ptcli/main.py b/pillar_tool/ptcli/main.py index edc3e1f..09b82a3 100644 --- a/pillar_tool/ptcli/main.py +++ b/pillar_tool/ptcli/main.py @@ -67,7 +67,8 @@ def host_list(): response = requests.get(f"{base_url}/host", headers=auth_header) response.raise_for_status() - print(response.json()) + for h in response.json(): + click.echo(f" - {h}") except requests.exceptions.HTTPError as e: raise click.ClickException(f"Failed to list hosts:\n{e}") @@ -108,7 +109,15 @@ def host_delete(fqdn: str): @hostgroup.command("list") def hostgroup_list(): click.echo("Listing known hostgroups...") - click.echo("TODO: implement") + try: + response = requests.get(f'{base_url}/hostgroup', headers=auth_header) + response.raise_for_status() + + click.echo("Hostgroups:") + for hg in response.json(): + click.echo(f" - {hg}") + except requests.exceptions.HTTPError as e: + raise click.ClickException(f"Failed to list hostgroups:\n{e}") @hostgroup.command("create") diff --git a/pillar_tool/routers/host.py b/pillar_tool/routers/host.py index 8a5b588..259663a 100644 --- a/pillar_tool/routers/host.py +++ b/pillar_tool/routers/host.py @@ -172,6 +172,22 @@ async def host_add(request: Request, fqdn: str, params: HostCreateParams): @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 diff --git a/pillar_tool/routers/hostgroup.py b/pillar_tool/routers/hostgroup.py index 54ef756..abf5523 100644 --- a/pillar_tool/routers/hostgroup.py +++ b/pillar_tool/routers/hostgroup.py @@ -1,7 +1,15 @@ +from http.client import HTTPResponse + +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.schemas import HostgroupCreateParams +from pillar_tool.util.validation import split_and_validate_path router = APIRouter( prefix="/hostgroup", @@ -11,24 +19,134 @@ router = APIRouter( @router.get("") def hostgroups_get(req: Request): - pass + """ + Retrieve all host groups. + + Fetches and returns a list of host group names from the database. + A host group is defined as a `Host` record where `is_hostgroup == True`. + + Returns: + JSONResponse: A JSON response with status code 200 containing a list of host group names (strings). + """ + db: Session = req.state.db + + result = db.execute(select(Host).where(Host.is_hostgroup == True)).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("/{name}") -def hostgroup_get(req: Request, name: str): - pass +def hostgroup_get(req: Request, name: str, path_input: str | None = None): + """ + Retrieve a specific host group by name. + + Fetches and returns details of the specified host group. + Returns 404 if no such host group exists. + + Args: + req (Request): The incoming request object. + name (str): The name of the host group to retrieve. + path_input (str): the path of the group if desired + + Returns: + JSONResponse: A JSON response with status code 200 and the host group details on success, + or 404 if not found. + """ + db: Session = req.state.db + + # decode the path + last = None + ancestors = [] + if path_input is not None: + path = split_and_validate_path(path_input) + + # get the path from the db + path_stmt = select(Host).where(Host.name == bindparam('name') and Host.parent_id == bindparam('parent_id')) + for label in path: + result = db.execute(path_stmt, {'name': label, 'parent_id': last}).fetchall() + + # error 404 if there is no matching item + if len(result) != 1: + raise HTTPException(status_code=404, detail="No such hostgroup path exists") + + tmp: Host = result[0][0] + ancestors.append(tmp) + last = tmp.id + + # get the host in question + stmt = select(Host).where(Host.name == name and Host.is_hostgroup == True and Host.parent_id == last) + result = db.execute(stmt).fetchall() + + if len(result) == 0: + raise HTTPException(status_code=404, detail="No such hostgroup exists") + + # Note: this should be enforced by the database + assert len(result) == 1 + print("check 1") + + hg: Host = result[0][0] + + return JSONResponse(status_code=200, content={ + 'hostgroup': hg.name, + 'path': '/'.join(x.name for x in ancestors) + }) @router.post("/{name}") def hostgroup_create(req: Request, name: str, params: HostgroupCreateParams): + """ + Create a new host group. + + Creates a new host group record in the database with the provided parameters. + + Args: + req (Request): The incoming request object. + name (str): The name of the host group (used as identifier). + params (HostgroupCreateParams): The creation parameters (e.g., description, associated hosts). + + Returns: + JSONResponse: A JSON response with status code 201 on success, + or appropriate error codes (e.g., 409 if already exists). + """ pass @router.patch("/{name}") def hostgroup_update(req: Request, name: str, params: HostgroupCreateParams): + """ + Update an existing host group by name. + + Updates the specified host group with new parameters. + Returns 404 if no such host group exists. + + Args: + req (Request): The incoming request object. + name (str): The current name of the host group to update. + params (HostgroupCreateParams): The updated parameters. + + Returns: + JSONResponse: A JSON response with status code 200 on success, + or 404 if not found. + """ pass @router.delete("/{name}") def hostgroup_delete(req: Request, name: str, params: HostgroupCreateParams): + """ + Delete a host group by name. + + Deletes the specified host group from the database. + Returns 404 if no such host group exists. + + Args: + req (Request): The incoming request object. + name (str): The name of the host group to delete. + params (HostgroupCreateParams): Included for consistency but typically unused in deletions. + + Returns: + JSONResponse: A JSON response with status code 204 on successful deletion, + or 404 if not found. + """ pass