Refactor pillar model, add Environment and State models and add some basic API endpoints

This commit is contained in:
Linus Vogel 2026-02-08 20:55:55 +01:00
parent b37f78b961
commit c47794ecda
6 changed files with 170 additions and 26 deletions

View File

@ -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 ###

View File

@ -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 ###

View File

@ -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'),
)

View File

@ -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"),
)

View File

@ -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

View File

@ -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}")
output.update({
"parent": params.parent
})
return JSONResponse(output)
@app.get("/top/{fqdn}")
async def host_top(request: Request, fqdn: str):
# TODO: implement
return JSONResponse({})