from datetime import datetime from enum import Enum from typing import Any from sqlalchemy import ForeignKey, func, text from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import Mapped, mapped_column, relationship from src.models import CustomBase, TimestampMixin, IdMixin, DeletedTimestampMixin class TofuInstanceStatus(Enum): ACTIVE = "ACTIVE" DEPLOYING = "DEPLOYING" DESTROYED = "DESTROYED" DESTROYING = "DESTROYING" DRIFTED = "DRIFTED" FAILED = "FAILED" FAILED_DESTROY = "FAILED_DESTROY" PENDING = "PENDING" PENDING_DESTROY = "PENDING_DESTROY" PENDING_DRIFT_CHECK = "PENDING_DRIFT_CHECK" class TofuInstance(CustomBase, IdMixin, TimestampMixin, DeletedTimestampMixin): __tablename__ = "tofu_instance" status: Mapped[TofuInstanceStatus] = mapped_column(default=TofuInstanceStatus.PENDING) configuration: Mapped[dict[str, Any]] outputs: Mapped[dict[str, Any] | None] plan: Mapped[dict[str, Any] | None] state: Mapped[dict[str, Any] | None] state_password: Mapped[bytes | None] state_lock: Mapped[dict[str, Any] | None] status_changed_at: Mapped[datetime] = mapped_column(default=func.now()) drift_checked_at: Mapped[datetime | None] tasks = relationship("TofuInstanceTask", back_populates="instance") status_changes = relationship("TofuInstanceStatusChange", back_populates="instance") class TofuInstanceStatusChange(CustomBase, IdMixin): __tablename__ = "tofu_instance_status_change" instance_id: Mapped[int] = mapped_column(ForeignKey("tofu_instance.id")) instance_task_id: Mapped[int] = mapped_column(ForeignKey("tofu_instance_task.id")) timestamp: Mapped[datetime] = mapped_column(default=func.now()) old_status: Mapped[TofuInstanceStatus] new_status: Mapped[TofuInstanceStatus] instance = relationship("TofuInstance", back_populates="status_changes") class TofuInstanceTaskType(Enum): CHECK_DRIFT = "CHECK_DRIFT" DEPLOY = "DEPLOY" DESTROY = "DESTROY" class TofuInstanceTaskStatus(Enum): CANCELED = "CANCELED" COMPLETED = "COMPLETED" FAILED = "FAILED" PENDING = "PENDING" RUNNING = "RUNNING" class TofuInstanceTask(CustomBase, IdMixin, TimestampMixin): __tablename__ = "tofu_instance_task" instance_id: Mapped[int] = mapped_column(ForeignKey("tofu_instance.id")) task: Mapped[TofuInstanceTaskType] status: Mapped[TofuInstanceTaskStatus] = mapped_column( default=TofuInstanceTaskStatus.PENDING ) start_time: Mapped[datetime | None] end_time: Mapped[datetime | None] instance = relationship("TofuInstance", back_populates="tasks") class TofuInstanceTaskLog(CustomBase, IdMixin): __tablename__ = "tofu_instance_task_log" instance_task_id: Mapped[int] = mapped_column(ForeignKey("tofu_instance_task.id")) timestamp: Mapped[datetime] = mapped_column(default=func.now()) log: Mapped[dict[str, Any]] class TofuBruteForce(CustomBase, IdMixin, TimestampMixin): __tablename__ = "tofu_brute_force" host: Mapped[str] expiry: Mapped[datetime] = mapped_column(default=func.now() + text("INTERVAL '1 hour'")) def update_tofu_instance_status( db: AsyncSession, instance: TofuInstance, task_id: int, new_status: TofuInstanceStatus ) -> None: status_change = TofuInstanceStatusChange( instance_id=instance.id, instance_task_id=task_id, old_status=instance.status, new_status=new_status, ) db.add(status_change) instance.status = new_status instance.status_changed_at = func.now() if new_status == TofuInstanceStatus.DESTROYED: instance.deleted_at = func.now()