Include full contents of all nested repositories

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-27 16:25:02 +01:00
parent 14ff8fd54c
commit 2401ed446f
7271 changed files with 1310112 additions and 6 deletions

View File

@@ -0,0 +1,21 @@
"""SQLAlchemy models for the Orchestrator."""
from app.models.base import Base
from app.models.tenant import Tenant
from app.models.server import Server
from app.models.task import Task, TaskStatus
from app.models.agent import Agent, AgentStatus
from app.models.event import Event
from app.models.registration_token import RegistrationToken
__all__ = [
"Base",
"Tenant",
"Server",
"Task",
"TaskStatus",
"Agent",
"AgentStatus",
"Event",
"RegistrationToken",
]

View File

@@ -0,0 +1,92 @@
"""Agent model for SysAdmin automation workers."""
import uuid
from datetime import datetime
from enum import Enum
from typing import TYPE_CHECKING
from sqlalchemy import DateTime, ForeignKey, String, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base, TimestampMixin, UUIDMixin
if TYPE_CHECKING:
from app.models.registration_token import RegistrationToken
from app.models.task import Task
from app.models.tenant import Tenant
class AgentStatus(str, Enum):
"""Agent status values."""
ONLINE = "online"
OFFLINE = "offline"
INVALID = "invalid" # Agent with NULL tenant_id, must re-register
class Agent(UUIDMixin, TimestampMixin, Base):
"""
Agent model representing a SysAdmin automation worker.
Agents register with the orchestrator and receive tasks to execute.
"""
__tablename__ = "agents"
tenant_id: Mapped[uuid.UUID | None] = mapped_column(
ForeignKey("tenants.id", ondelete="CASCADE"),
nullable=True,
index=True,
)
name: Mapped[str] = mapped_column(
String(255),
nullable=False,
)
version: Mapped[str] = mapped_column(
String(50),
nullable=False,
default="",
)
status: Mapped[str] = mapped_column(
String(20),
nullable=False,
default=AgentStatus.OFFLINE.value,
index=True,
)
last_heartbeat: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True),
nullable=True,
)
# Legacy field - kept for backward compatibility during migration
# Will be removed after all agents migrate to new auth scheme
token: Mapped[str] = mapped_column(
Text,
nullable=False,
default="",
)
# New secure credential storage - SHA-256 hash of agent secret
secret_hash: Mapped[str] = mapped_column(
String(64),
nullable=False,
default="",
comment="SHA-256 hash of the agent secret",
)
# Reference to the registration token used to create this agent
registration_token_id: Mapped[uuid.UUID | None] = mapped_column(
ForeignKey("registration_tokens.id", ondelete="SET NULL"),
nullable=True,
index=True,
)
# Relationships
tenant: Mapped["Tenant | None"] = relationship(
back_populates="agents",
)
tasks: Mapped[list["Task"]] = relationship(
back_populates="agent",
lazy="selectin",
)
registration_token: Mapped["RegistrationToken | None"] = relationship()
def __repr__(self) -> str:
return f"<Agent(id={self.id}, name={self.name}, status={self.status})>"

View File

@@ -0,0 +1,44 @@
"""Base model and mixins for SQLAlchemy ORM."""
import uuid
from datetime import datetime, timezone
from sqlalchemy import DateTime
from sqlalchemy.ext.asyncio import AsyncAttrs
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
def utc_now() -> datetime:
"""Return current UTC datetime."""
return datetime.now(timezone.utc)
class Base(AsyncAttrs, DeclarativeBase):
"""Base class for all SQLAlchemy models."""
pass
class UUIDMixin:
"""Mixin that adds a UUID primary key."""
id: Mapped[uuid.UUID] = mapped_column(
primary_key=True,
default=uuid.uuid4,
)
class TimestampMixin:
"""Mixin that adds created_at and updated_at timestamps."""
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
default=utc_now,
nullable=False,
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
default=utc_now,
onupdate=utc_now,
nullable=False,
)

View File

@@ -0,0 +1,72 @@
"""Event model for audit logging."""
import uuid
from datetime import datetime, timezone
from typing import TYPE_CHECKING, Any
from sqlalchemy import DateTime, ForeignKey, JSON, String
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column, relationship
# Use JSONB on PostgreSQL, JSON on other databases (SQLite for tests)
JSONType = JSON().with_variant(JSONB, "postgresql")
from app.models.base import Base, UUIDMixin
if TYPE_CHECKING:
from app.models.task import Task
from app.models.tenant import Tenant
def utc_now() -> datetime:
"""Return current UTC datetime."""
return datetime.now(timezone.utc)
class Event(UUIDMixin, Base):
"""
Event model for audit logging and activity tracking.
Events are immutable records of system activity.
Only has created_at (no updated_at since events are immutable).
"""
__tablename__ = "events"
tenant_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("tenants.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
task_id: Mapped[uuid.UUID | None] = mapped_column(
ForeignKey("tasks.id", ondelete="SET NULL"),
nullable=True,
index=True,
)
event_type: Mapped[str] = mapped_column(
String(100),
nullable=False,
index=True,
)
payload: Mapped[dict[str, Any]] = mapped_column(
JSONType,
nullable=False,
default=dict,
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
default=utc_now,
nullable=False,
index=True,
)
# Relationships
tenant: Mapped["Tenant"] = relationship(
back_populates="events",
)
task: Mapped["Task | None"] = relationship(
back_populates="events",
)
def __repr__(self) -> str:
return f"<Event(id={self.id}, type={self.event_type})>"

View File

@@ -0,0 +1,101 @@
"""Registration token model for secure agent registration."""
import uuid
from datetime import datetime
from typing import TYPE_CHECKING
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base, TimestampMixin, UUIDMixin
if TYPE_CHECKING:
from app.models.tenant import Tenant
class RegistrationToken(UUIDMixin, TimestampMixin, Base):
"""
Registration token for secure agent registration.
Tokens are pre-provisioned by admins and map to specific tenants.
Agents use these tokens during initial registration to:
1. Authenticate the registration request
2. Associate themselves with the correct tenant
Tokens can be:
- Single-use (max_uses=1, default)
- Limited-use (max_uses > 1)
- Unlimited (max_uses=0)
- Time-limited (expires_at set)
- Manually revoked (revoked=True)
"""
__tablename__ = "registration_tokens"
tenant_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("tenants.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
token_hash: Mapped[str] = mapped_column(
String(64),
nullable=False,
index=True,
comment="SHA-256 hash of the registration token",
)
description: Mapped[str | None] = mapped_column(
String(255),
nullable=True,
comment="Human-readable description for the token",
)
max_uses: Mapped[int] = mapped_column(
Integer,
nullable=False,
default=1,
comment="Maximum number of uses (0 = unlimited)",
)
use_count: Mapped[int] = mapped_column(
Integer,
nullable=False,
default=0,
comment="Current number of times this token has been used",
)
expires_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True),
nullable=True,
comment="Optional expiration timestamp",
)
revoked: Mapped[bool] = mapped_column(
Boolean,
nullable=False,
default=False,
comment="Whether this token has been manually revoked",
)
created_by: Mapped[str | None] = mapped_column(
String(255),
nullable=True,
comment="Identifier of who created this token (for audit)",
)
# Relationships
tenant: Mapped["Tenant"] = relationship(
back_populates="registration_tokens",
)
def __repr__(self) -> str:
return f"<RegistrationToken(id={self.id}, tenant_id={self.tenant_id}, uses={self.use_count}/{self.max_uses})>"
def is_valid(self, now: datetime | None = None) -> bool:
"""Check if the token can still be used for registration."""
from app.models.base import utc_now
if now is None:
now = utc_now()
if self.revoked:
return False
if self.expires_at is not None and self.expires_at < now:
return False
if self.max_uses > 0 and self.use_count >= self.max_uses:
return False
return True

View File

@@ -0,0 +1,59 @@
"""Server model for provisioned infrastructure."""
import uuid
from enum import Enum
from typing import TYPE_CHECKING
from sqlalchemy import ForeignKey, String
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base, TimestampMixin, UUIDMixin
if TYPE_CHECKING:
from app.models.tenant import Tenant
class ServerStatus(str, Enum):
"""Server provisioning status."""
PROVISIONING = "provisioning"
READY = "ready"
ERROR = "error"
TERMINATED = "terminated"
class Server(UUIDMixin, TimestampMixin, Base):
"""
Server model representing a provisioned VM or container.
Tracks provisioning state and network configuration.
"""
__tablename__ = "servers"
tenant_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("tenants.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
hostname: Mapped[str] = mapped_column(
String(255),
nullable=False,
)
ip_address: Mapped[str | None] = mapped_column(
String(45), # Supports IPv6
nullable=True,
)
status: Mapped[str] = mapped_column(
String(50),
default=ServerStatus.PROVISIONING.value,
nullable=False,
)
# Relationships
tenant: Mapped["Tenant"] = relationship(
back_populates="servers",
)
def __repr__(self) -> str:
return f"<Server(id={self.id}, hostname={self.hostname}, status={self.status})>"

View File

@@ -0,0 +1,85 @@
"""Task model for orchestration jobs."""
import uuid
from enum import Enum
from typing import TYPE_CHECKING, Any
from sqlalchemy import ForeignKey, JSON, String
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column, relationship
# Use JSONB on PostgreSQL, JSON on other databases (SQLite for tests)
JSONType = JSON().with_variant(JSONB, "postgresql")
from app.models.base import Base, TimestampMixin, UUIDMixin
if TYPE_CHECKING:
from app.models.agent import Agent
from app.models.event import Event
from app.models.tenant import Tenant
class TaskStatus(str, Enum):
"""Task execution status."""
PENDING = "pending"
RUNNING = "running"
COMPLETED = "completed"
FAILED = "failed"
class Task(UUIDMixin, TimestampMixin, Base):
"""
Task model representing an orchestration job.
Tasks are assigned to agents and track execution state.
Payload and result use JSONB for flexible, queryable storage.
"""
__tablename__ = "tasks"
tenant_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("tenants.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
agent_id: Mapped[uuid.UUID | None] = mapped_column(
ForeignKey("agents.id", ondelete="SET NULL"),
nullable=True,
index=True,
)
type: Mapped[str] = mapped_column(
String(100),
nullable=False,
index=True,
)
payload: Mapped[dict[str, Any]] = mapped_column(
JSONType,
nullable=False,
default=dict,
)
status: Mapped[str] = mapped_column(
String(50),
default=TaskStatus.PENDING.value,
nullable=False,
index=True,
)
result: Mapped[dict[str, Any] | None] = mapped_column(
JSONType,
nullable=True,
)
# Relationships
tenant: Mapped["Tenant"] = relationship(
back_populates="tasks",
)
agent: Mapped["Agent | None"] = relationship(
back_populates="tasks",
)
events: Mapped[list["Event"]] = relationship(
back_populates="task",
lazy="selectin",
)
def __repr__(self) -> str:
return f"<Task(id={self.id}, type={self.type}, status={self.status})>"

View File

@@ -0,0 +1,67 @@
"""Tenant model for multi-tenancy support."""
from typing import TYPE_CHECKING
from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base, TimestampMixin, UUIDMixin
if TYPE_CHECKING:
from app.models.agent import Agent
from app.models.event import Event
from app.models.registration_token import RegistrationToken
from app.models.server import Server
from app.models.task import Task
class Tenant(UUIDMixin, TimestampMixin, Base):
"""
Tenant model representing a customer organization.
Each tenant has isolated servers, tasks, agents, and events.
"""
__tablename__ = "tenants"
name: Mapped[str] = mapped_column(
String(255),
unique=True,
nullable=False,
index=True,
)
domain: Mapped[str | None] = mapped_column(
String(255),
unique=True,
nullable=True,
)
dashboard_token_hash: Mapped[str | None] = mapped_column(
String(64), # SHA-256 hex = 64 characters
nullable=True,
comment="SHA-256 hash of dashboard authentication token",
)
# Relationships
servers: Mapped[list["Server"]] = relationship(
back_populates="tenant",
lazy="selectin",
)
tasks: Mapped[list["Task"]] = relationship(
back_populates="tenant",
lazy="selectin",
)
agents: Mapped[list["Agent"]] = relationship(
back_populates="tenant",
lazy="selectin",
)
events: Mapped[list["Event"]] = relationship(
back_populates="tenant",
lazy="selectin",
)
registration_tokens: Mapped[list["RegistrationToken"]] = relationship(
back_populates="tenant",
lazy="selectin",
)
def __repr__(self) -> str:
return f"<Tenant(id={self.id}, name={self.name})>"