From c47794ecda7ab6e2123c71a5d8f8938a417c40f3 Mon Sep 17 00:00:00 2001 From: Linus Vogel Date: Sun, 8 Feb 2026 20:55:55 +0100 Subject: [PATCH] Refactor pillar model, add Environment and State models and add some basic API endpoints --- ...6_pillars_are_directly_assigned_to_the_.py | 62 +++++++++++++++++++ ...dded_environments_and_states_to_the_db_.py | 32 ++++++++++ pillar_tool/db/models/pillar_data.py | 21 +------ pillar_tool/db/models/top_data.py | 39 ++++++++++++ pillar_tool/db/queries/pillar_queries.py | 2 +- pillar_tool/main.py | 40 +++++++++--- 6 files changed, 170 insertions(+), 26 deletions(-) create mode 100644 pillar_tool/db/migrations/versions/2026_02_08_2034-7eb66922e256_pillars_are_directly_assigned_to_the_.py create mode 100644 pillar_tool/db/migrations/versions/2026_02_08_2051-0a912926be8b_added_environments_and_states_to_the_db_.py create mode 100644 pillar_tool/db/models/top_data.py diff --git a/pillar_tool/db/migrations/versions/2026_02_08_2034-7eb66922e256_pillars_are_directly_assigned_to_the_.py b/pillar_tool/db/migrations/versions/2026_02_08_2034-7eb66922e256_pillars_are_directly_assigned_to_the_.py new file mode 100644 index 0000000..104f543 --- /dev/null +++ b/pillar_tool/db/migrations/versions/2026_02_08_2034-7eb66922e256_pillars_are_directly_assigned_to_the_.py @@ -0,0 +1,62 @@ +"""pillars are directly assigned to the hosts + +Revision ID: 7eb66922e256 +Revises: 54537e95fc4d +Create Date: 2026-02-08 20:34:16.291415 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '7eb66922e256' +down_revision: Union[str, Sequence[str], None] = '54537e95fc4d' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('pillar_tool_pillar_value') + op.add_column('pillar_tool_pillar', sa.Column('pillar_name', sa.String(), nullable=False)) + op.add_column('pillar_tool_pillar', sa.Column('host_id', sa.UUID(), nullable=True)) + op.add_column('pillar_tool_pillar', sa.Column('type', sa.String(), nullable=False)) + op.add_column('pillar_tool_pillar', sa.Column('value', sa.String(), nullable=False)) + op.drop_constraint(op.f('pillar_parent_unique_name_parent'), 'pillar_tool_pillar', type_='unique') + op.create_unique_constraint('pillar_unique_pillar_name', 'pillar_tool_pillar', ['pillar_name', 'host_id']) + op.drop_constraint(op.f('pillar_tool_pillar_parent_id_fkey'), 'pillar_tool_pillar', type_='foreignkey') + op.create_foreign_key(None, 'pillar_tool_pillar', 'pillar_tool_host', ['host_id'], ['id']) + op.drop_column('pillar_tool_pillar', 'name') + op.drop_column('pillar_tool_pillar', 'parent_id') + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('pillar_tool_pillar', sa.Column('parent_id', sa.UUID(), autoincrement=False, nullable=True)) + op.add_column('pillar_tool_pillar', sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=False)) + op.drop_constraint(None, 'pillar_tool_pillar', type_='foreignkey') + op.create_foreign_key(op.f('pillar_tool_pillar_parent_id_fkey'), 'pillar_tool_pillar', 'pillar_tool_pillar', ['parent_id'], ['id']) + op.drop_constraint('pillar_unique_pillar_name', 'pillar_tool_pillar', type_='unique') + op.create_unique_constraint(op.f('pillar_parent_unique_name_parent'), 'pillar_tool_pillar', ['parent_id', 'name'], postgresql_nulls_not_distinct=False) + op.drop_column('pillar_tool_pillar', 'value') + op.drop_column('pillar_tool_pillar', 'type') + op.drop_column('pillar_tool_pillar', 'host_id') + op.drop_column('pillar_tool_pillar', 'pillar_name') + op.create_table('pillar_tool_pillar_value', + sa.Column('id', sa.UUID(), autoincrement=False, nullable=False), + sa.Column('pillar_id', sa.UUID(), autoincrement=False, nullable=False), + sa.Column('host_id', sa.UUID(), autoincrement=False, nullable=True), + sa.Column('type', sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column('value', sa.VARCHAR(), autoincrement=False, nullable=False), + sa.ForeignKeyConstraint(['host_id'], ['pillar_tool_host.id'], name=op.f('pillar_tool_pillar_value_host_id_fkey')), + sa.ForeignKeyConstraint(['pillar_id'], ['pillar_tool_pillar.id'], name=op.f('pillar_tool_pillar_value_pillar_id_fkey')), + sa.PrimaryKeyConstraint('id', name=op.f('pillar_tool_pillar_value_pkey')), + sa.UniqueConstraint('pillar_id', 'host_id', name=op.f('pillar_value_unique_pillar_value'), postgresql_include=[], postgresql_nulls_not_distinct=False) + ) + # ### end Alembic commands ### diff --git a/pillar_tool/db/migrations/versions/2026_02_08_2051-0a912926be8b_added_environments_and_states_to_the_db_.py b/pillar_tool/db/migrations/versions/2026_02_08_2051-0a912926be8b_added_environments_and_states_to_the_db_.py new file mode 100644 index 0000000..9ffc6b6 --- /dev/null +++ b/pillar_tool/db/migrations/versions/2026_02_08_2051-0a912926be8b_added_environments_and_states_to_the_db_.py @@ -0,0 +1,32 @@ +"""added environments and states to the db schema + +Revision ID: 0a912926be8b +Revises: 7eb66922e256 +Create Date: 2026-02-08 20:51:10.862477 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '0a912926be8b' +down_revision: Union[str, Sequence[str], None] = '7eb66922e256' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### diff --git a/pillar_tool/db/models/pillar_data.py b/pillar_tool/db/models/pillar_data.py index 3246504..f82385b 100644 --- a/pillar_tool/db/models/pillar_data.py +++ b/pillar_tool/db/models/pillar_data.py @@ -5,32 +5,17 @@ from sqlalchemy import Column, UUID, String, ForeignKey, UniqueConstraint, Boole class Pillar(Base): """ - Describes a pillar by its name and parent. A parent equal to NULL mains that the pillar is a top level pillar. - Note that this is not a value, as the value is bound to the host. + A value for a given pillar on a given host. """ __tablename__ = 'pillar_tool_pillar' id = Column(UUID, primary_key=True) - name = Column(String, nullable=False) - parent_id = Column(UUID, ForeignKey('pillar_tool_pillar.id'), nullable=True) - - __table_args__ = ( - UniqueConstraint('parent_id', 'name', name="pillar_parent_unique_name_parent"), - ) - - -class PillarValue(Base): - """ - A value for a given pillar on a given host. - """ - __tablename__ = 'pillar_tool_pillar_value' - id = Column(UUID, primary_key=True) - pillar_id = Column(UUID, ForeignKey('pillar_tool_pillar.id'), nullable=False) + pillar_name = Column(String, nullable=False) host_id = Column(UUID, ForeignKey('pillar_tool_host.id'), nullable=True) type = Column(String, nullable=False) value = Column(String, nullable=False) __table_args__ = ( - UniqueConstraint('pillar_id', 'host_id', name='pillar_value_unique_pillar_value'), + UniqueConstraint('pillar_name', 'host_id', name='pillar_unique_pillar_name'), ) diff --git a/pillar_tool/db/models/top_data.py b/pillar_tool/db/models/top_data.py new file mode 100644 index 0000000..7961dc8 --- /dev/null +++ b/pillar_tool/db/models/top_data.py @@ -0,0 +1,39 @@ +from pillar_tool.db.base_model import Base + +from sqlalchemy import Column, UUID, String, ForeignKey, UniqueConstraint, Boolean +import uuid + + +class Environment(Base): + __tablename__ = "pillar_tool_environment" + + id = Column(UUID, primary_key=True, default=uuid.uuid4) + name = Column(String, nullable=False) + + __table_args__ = ( + UniqueConstraint('name', name="pillar_tool_unique_environment_unique_name") + ) + + +class State(Base): + __tablename__ = "pillar_tool_state" + + id = Column(UUID, primary_key=True, default=uuid.uuid4) + name = Column(String, nullable=False) + + __table_args__ = ( + UniqueConstraint('name', name="pillar_tool_unique_state_unique_name") + ) + + +class StateAssignment(Base): + __tablename__ = "pillar_tool_state_assignment" + + id = Column(UUID, primary_key=True, default=uuid.uuid4) + environment_id = Column(UUID, ForeignKey("pillar_tool_environment.id"), nullable=False) + state_id = Column(UUID, ForeignKey("pillar_tool_state.id"), nullable=False) + host_id = Column(UUID, ForeignKey("pillar_tool_host.id"), nullable=False) + + __table_args__ = ( + UniqueConstraint('environment_id', 'state_id', 'host_id', name="pillar_tool_state_assignment_unique_env_state_host"), + ) \ No newline at end of file diff --git a/pillar_tool/db/queries/pillar_queries.py b/pillar_tool/db/queries/pillar_queries.py index e538507..9ab218c 100644 --- a/pillar_tool/db/queries/pillar_queries.py +++ b/pillar_tool/db/queries/pillar_queries.py @@ -23,7 +23,7 @@ def generate_host_hierarchy(db: Session, labels: list[str]) -> list[Host]: # NOTE: this is an assertion because the schema should enforce this assert len(result) == 1 instance = Host(result[0]) - print(instance.id) + last_parent_id = instance.id out.append(instance) return out diff --git a/pillar_tool/main.py b/pillar_tool/main.py index c596fbe..642f171 100644 --- a/pillar_tool/main.py +++ b/pillar_tool/main.py @@ -1,11 +1,12 @@ # load config so everything else can work +from pillar_tool.util import load_config, config +load_config() + from http.server import BaseHTTPRequestHandler from pillar_tool.db.base_model import as_dict from pillar_tool.middleware.logging import request_logging_middleware from pillar_tool.schemas import HostCreateParams -from pillar_tool.util import load_config, config -load_config() from starlette.middleware.base import BaseHTTPMiddleware @@ -84,17 +85,42 @@ async def pillar_get(req: Request, host: str): @app.post("/pillar/{host}") async def pillar_set(request: Request, host: str, value: str): - print(request.headers) + return JSONResponse({ + "captain.linvogel.internal": { + "states": ["state1", "state2"], + "test": { + "pillar": "value" + } + } + }) @app.get("/hosts") async def host_list(request: Request): all_hosts = list_all_hosts(request.state.db) - return JSONResponse([as_dict(x) for x in all_hosts]) + return JSONResponse([x.name for x in all_hosts if x.parent_id is None]) + +@app.get("/hostgroups") +async def hostgroup_list(request: Request): + all_hosts = list_all_hosts(request.state.db) + return JSONResponse([x.name for x in all_hosts if x.parent_id is not None]) @app.post("/host/{fqdn}") async def host_add(request: Request, fqdn: str, params: HostCreateParams): new_host = create_host(request.state.db, fqdn, params.parent) + + output = { + "message": "Host created", + "host": new_host, + } if params.parent: - print(f"Created new host: {new_host} with parent: {params.parent}") - else: - print(f"Created new host: {new_host}") \ No newline at end of file + output.update({ + "parent": params.parent + }) + + return JSONResponse(output) + +@app.get("/top/{fqdn}") +async def host_top(request: Request, fqdn: str): + # TODO: implement + return JSONResponse({}) +