"""Update job + log line models. Job lifecycle (pull model): pending -> claimed (satellite picked it up) -> running -> success | failed | cancelled A claimed/running job whose satellite goes silent past the stale timeout is marked failed by the janitor. """ import enum from datetime import UTC, datetime from sqlalchemy import DateTime, Enum, ForeignKey, Integer, String, Text from sqlalchemy.orm import Mapped, mapped_column, relationship from app.core.database import Base class JobStatus(enum.StrEnum): PENDING = "pending" CLAIMED = "claimed" RUNNING = "running" SUCCESS = "success" FAILED = "failed" CANCELLED = "cancelled" class JobType(enum.StrEnum): WINDOWS_UPDATE = "windows_update" LINUX_UPDATE = "linux_update" CAU_RUN = "cau_run" HEALTH_CHECK = "health_check" NETWORK_SCAN = "network_scan" class UpdateJob(Base): __tablename__ = "update_jobs" id: Mapped[int] = mapped_column(primary_key=True) customer_id: Mapped[int] = mapped_column( ForeignKey("customers.id"), index=True ) customer: Mapped["Customer"] = relationship(back_populates="jobs", lazy="selectin") # noqa: F821 # Null for NETWORK_SCAN jobs (target = whole local network) server_id: Mapped[int | None] = mapped_column( ForeignKey("servers.id"), nullable=True, index=True ) server: Mapped["Server | None"] = relationship(back_populates="jobs", lazy="selectin") # noqa: F821 # Set when a satellite claims the job satellite_id: Mapped[int | None] = mapped_column( ForeignKey("satellites.id"), nullable=True, index=True ) satellite: Mapped["Satellite | None"] = relationship(back_populates="jobs", lazy="selectin") # noqa: F821 type: Mapped[JobType] = mapped_column(Enum(JobType)) status: Mapped[JobStatus] = mapped_column(Enum(JobStatus), default=JobStatus.PENDING, index=True) progress_percent: Mapped[int] = mapped_column(Integer, default=0) current_phase: Mapped[str | None] = mapped_column(String(255), nullable=True) # Optional job parameters (e.g. reboot_if_required, scan_subnet) params: Mapped[str | None] = mapped_column(Text, nullable=True) # JSON blob created_by: Mapped[str] = mapped_column(String(255)) # dashboard username claimed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) last_report_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) error: Mapped[str | None] = mapped_column(Text, nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(UTC) ) logs: Mapped[list["UpdateLog"]] = relationship( back_populates="job", cascade="all, delete-orphan", order_by="UpdateLog.id" ) class UpdateLog(Base): __tablename__ = "update_logs" id: Mapped[int] = mapped_column(primary_key=True) job_id: Mapped[int] = mapped_column(ForeignKey("update_jobs.id"), index=True) job: Mapped[UpdateJob] = relationship(back_populates="logs") timestamp: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(UTC) ) level: Mapped[str] = mapped_column(String(20), default="info") line: Mapped[str] = mapped_column(Text)