worked on pillar endpoints

This commit is contained in:
Linus Vogel 2026-02-16 22:43:49 +01:00
parent ace0062f37
commit 37fa6bcbb3
5 changed files with 87 additions and 66 deletions

View File

@ -1,7 +1,9 @@
from typing import Any import json
from collections import defaultdict
from pillar_tool.db.models.pillar_data import * from pillar_tool.db.models.pillar_data import *
from uuid import UUID
from sqlalchemy import select, insert, union from sqlalchemy import select, insert, union
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
@ -9,57 +11,32 @@ from sqlalchemy.orm import Session
def get_pillar_name_sequence(name: str) -> list[str]: def get_pillar_name_sequence(name: str) -> list[str]:
return name.split(':') return name.split(':')
def decode_pillar_value(pillar: Pillar) -> str | int | float | bool | list | dict:
def generate_host_hierarchy(db: Session, labels: list[str]) -> list[Host]: match pillar.type:
path_consumed = [] case 'string': return pillar.value
out = [] case 'integer': return int(pillar.value)
last_parent_id = None case 'float': return float(pillar.value)
for label in labels: case 'boolean': return bool(pillar.value)
path_consumed += label case 'array': return json.loads(pillar.value)
stmt = select(Host).where(Host.name == label and Host.parent_id == last_parent_id) case 'dict': return json.loads(pillar.value)
result = list(db.execute(stmt).fetchall()) raise RuntimeError(f"Failed to decode pillar value: Invalid type '{pillar.type}'")
if not result:
raise RuntimeError(f"No such host(-group): '{':'.join(path_consumed)}'")
# NOTE: this is an assertion because the schema should enforce this
assert len(result) == 1
instance = Host(result[0])
last_parent_id = instance.id
out.append(instance)
return out
def get_values_for_host(db: Session, host: str) -> dict: def get_pillar_for_target(db: Session, target: UUID) -> dict:
labels = get_pillar_name_sequence(host) pillar_stmt = select(Pillar).where(Pillar.host_id == target)
hierarchy = generate_host_hierarchy(db, labels) result = db.execute(pillar_stmt).fetchall()
# TODO: generate host hierarchy out = {}
# TODO: find all values assigned o this host hierarchy and sort by depth for row in result:
# TODO: build the pillar structure row: Pillar = row[0]
name = row.pillar_name
return {} value = decode_pillar_value(row)
labels = get_pillar_name_sequence(name)
current = out
def create_pillar_host(db: Session, host_id: UUID, name: str, value: Any) -> None: l = len(labels)
# TODO: generate host hierarchy for i, label in enumerate(labels):
# get the involved host or hostgroup if label not in current:
res = db.execute(select(Host).where(Host.id == host_id)).fetchone() current[label] = {} if i < l-1 else value
if res is None:
# TODO: handle this error with a custom Exception
raise RuntimeError(f"No Host or Hostgroup with id {host_id} exists!")
host = res[0][0]
# TODO: generate pillar path from name
# TODO: find if pillar already exists
# TODO: create new pillar if it doesn't exist
# TODO: assign value to new or existing pillar
return
def create_pillar_host_group(db: Session, host_group: UUID, name: str, value: Any) -> None:
pass
print(json.dumps(out, indent=4))
pass

View File

@ -27,6 +27,7 @@ from pillar_tool.routers.host import router as host_router
from pillar_tool.routers.hostgroup import router as hostgroup_router from pillar_tool.routers.hostgroup import router as hostgroup_router
from pillar_tool.routers.environment import router as environment_router from pillar_tool.routers.environment import router as environment_router
from pillar_tool.routers.state import router as state_router from pillar_tool.routers.state import router as state_router
from pillar_tool.routers.pillar import router as pillar_router
# run any pending migrations # run any pending migrations
run_db_migrations() run_db_migrations()
@ -69,11 +70,12 @@ app.add_middleware(BaseHTTPMiddleware, dispatch=db_connection_middleware)
app.add_middleware(BaseHTTPMiddleware, dispatch=request_logging_middleware) app.add_middleware(BaseHTTPMiddleware, dispatch=request_logging_middleware)
app.exception_handler(Exception)(on_general_error) app.exception_handler(Exception)(on_general_error)
# Setup the api router # Set up the api router
app.include_router(host_router) app.include_router(host_router)
app.include_router(hostgroup_router) app.include_router(hostgroup_router)
app.include_router(environment_router) app.include_router(environment_router)
app.include_router(state_router) app.include_router(state_router)
app.include_router(pillar_router)
@app.get("/") @app.get("/")
async def root(): async def root():

View File

@ -15,7 +15,7 @@ def hostgroup():
def hostgroup_list(): def hostgroup_list():
click.echo("Listing known hostgroups...") click.echo("Listing known hostgroups...")
try: try:
response = requests.get(f'{base_url}/hostgroup', headers=auth_header()) response = requests.get(f'{base_url()}/hostgroup', headers=auth_header())
response.raise_for_status() response.raise_for_status()
click.echo("Hostgroups:") click.echo("Hostgroups:")
@ -36,7 +36,7 @@ def hostgroup_show(path: str):
data = HostgroupParams( data = HostgroupParams(
path=path path=path
) )
response = requests.get(f'{base_url}/hostgroup/{name}', headers=auth_header(), params=data.model_dump()) response = requests.get(f'{base_url()}/hostgroup/{name}', headers=auth_header(), params=data.model_dump())
response.raise_for_status() response.raise_for_status()
except requests.exceptions.HTTPError as e: except requests.exceptions.HTTPError as e:
raise click.ClickException(f"Failed to show hostgroup:\n{e}") raise click.ClickException(f"Failed to show hostgroup:\n{e}")
@ -53,7 +53,7 @@ def hostgroup_create(path: str):
data = HostgroupParams( data = HostgroupParams(
path=path path=path
) )
response = requests.post(f'{base_url}/hostgroup/{name}', headers=auth_header(), json=data.model_dump()) response = requests.post(f'{base_url()}/hostgroup/{name}', headers=auth_header(), json=data.model_dump())
response.raise_for_status() response.raise_for_status()
except requests.exceptions.HTTPError as e: except requests.exceptions.HTTPError as e:
raise click.ClickException(f"Failed to create hostgroup:\n{e}") raise click.ClickException(f"Failed to create hostgroup:\n{e}")
@ -68,7 +68,7 @@ def hostgroup_delete(path: str):
name = labels[-1] name = labels[-1]
prefix = "/".join(labels[:-1]) if len(labels) > 1 else None prefix = "/".join(labels[:-1]) if len(labels) > 1 else None
query_params = f"?path={prefix}" if prefix is not None else '' query_params = f"?path={prefix}" if prefix is not None else ''
response = requests.delete(f'{base_url}/hostgroup/{name}{query_params}', headers=auth_header()) response = requests.delete(f'{base_url()}/hostgroup/{name}{query_params}', headers=auth_header())
response.raise_for_status() response.raise_for_status()
except requests.exceptions.HTTPError as e: except requests.exceptions.HTTPError as e:
raise click.ClickException(f"Failed to delete hostgroup:\n{e}") raise click.ClickException(f"Failed to delete hostgroup:\n{e}")

View File

@ -6,4 +6,20 @@ from .cli_main import main, auth_header, base_url
@main.group("pillar") @main.group("pillar")
def pillar(): def pillar():
pass pass
@pillar.command("get")
@click.argument("fqdn")
def pillar_get(fqdn):
"""Get pillar data for a given FQDN."""
try:
response = requests.get(
f"{base_url()}/pillar/{fqdn}",
headers=auth_header(),
)
response.raise_for_status()
pillar_data = response.json()
click.echo(pillar_data)
except requests.exceptions.RequestException as e:
click.echo(f"Error: {e}")

View File

@ -1,15 +1,17 @@
import uuid import uuid
from fastapi.params import Depends from sqlalchemy import select, insert, delete, bindparam
from sqlalchemy import select, insert, delete
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from starlette.exceptions import HTTPException from starlette.exceptions import HTTPException
from starlette.requests import Request from starlette.requests import Request
from fastapi import APIRouter from fastapi import APIRouter, Depends
from starlette.responses import JSONResponse from starlette.responses import JSONResponse
from pillar_tool.db import Host
from pillar_tool.db.models.top_data import State, StateAssignment from pillar_tool.db.models.top_data import State, StateAssignment
from pillar_tool.db.queries.pillar_queries import get_pillar_for_target
from pillar_tool.schemas import PillarParams, get_model_from_query from pillar_tool.schemas import PillarParams, get_model_from_query
from pillar_tool.util.pillar_utilities import merge
from pillar_tool.util.validation import validate_state_name from pillar_tool.util.validation import validate_state_name
router = APIRouter( router = APIRouter(
@ -19,8 +21,8 @@ router = APIRouter(
# Note: there is no list of all pillars, as this would not be helpful # Note: there is no list of all pillars, as this would not be helpful
@router.get("/{name}") @router.get("/{fqdn}")
def state_get(req: Request, name: str, params: Depends(get_model_from_query(PillarParams))): def pillar_get(req: Request, fqdn: str):
# TODO: implement # TODO: implement
# this function should: # this function should:
# - get the affected host hierarchy # - get the affected host hierarchy
@ -30,14 +32,38 @@ def state_get(req: Request, name: str, params: Depends(get_model_from_query(Pill
# if any error happens, return non-200 status and an empty dictionary so that salt does not shit itself # if any error happens, return non-200 status and an empty dictionary so that salt does not shit itself
db: Session = req.state.db db: Session = req.state.db
# get the host hierarchy
host_stmt = select(Host).where(Host.name == fqdn and Host.is_hostgroup == False)
result = db.execute(host_stmt).fetchall()
if len(result) == 0:
return JSONResponse(status=404, content={})
# NOTE: should be enforced by the database
assert len(result) == 1
@router.post("/{name}") host: Host = result[0][0]
def state_create(req: Request, name: str): path: list[Host] = [host]
parent_stmt = select(Host).where(Host.id == bindparam('parent'))
while path[-1].parent_id is not None:
result = db.execute(parent_stmt, {'parent': path[-1].parent_id}).fetchall()
# NOTE: should be enforced by the database
assert len(result) == 1
tmp: Host = result[0][0]
path.append(tmp)
path.reverse()
out = merge(get_pillar_for_target(db, host.id) for host in path)
return JSONResponse(status_code=200, content={})
@router.post("/{fqdn}")
def pillar_create(req: Request, fqdn: str, params: PillarParams):
# TODO: implement # TODO: implement
db = req.state.db db = req.state.db
@router.delete("/{name}") @router.delete("/{fqdn}")
def state_delete(req: Request, name: str): def pillar_delete(req: Request, fqdn: str):
# TODO: implement # TODO: implement
db = req.state.db db = req.state.db