from __future__ import annotations import enum import uuid from datetime import datetime, timezone from sqlalchemy import JSON, Boolean, DateTime, Enum, Float, ForeignKey, Integer, String, Text from sqlalchemy.orm import Mapped, mapped_column from .base import Base class AlertOperator(str, enum.Enum): gt = ">" lt = "<" gte = ">=" lte = "<=" eq = "==" ne = "!=" class AlertStateEnum(str, enum.Enum): inactive = "inactive" pending = "pending" firing = "firing" acknowledged = "acknowledged" resolved = "resolved" class AlertRule(Base): __tablename__ = "alert_rules" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4())) name: Mapped[str] = mapped_column(String(128), nullable=False) source_module: Mapped[str] = mapped_column(String(64), nullable=False) resource_name: Mapped[str] = mapped_column(String(255), nullable=False) metric_name: Mapped[str] = mapped_column(String(128), nullable=False) # The DB enum (migration 0002) stores the operator VALUES (">", "<", …), so # persist values rather than SQLAlchemy's default enum-member NAMES ("gt"…). operator: Mapped[AlertOperator] = mapped_column( Enum(AlertOperator, name="alertoperator", values_callable=lambda e: [m.value for m in e]), nullable=False, ) threshold: Mapped[float] = mapped_column(Float, nullable=False) consecutive_failures_required: Mapped[int] = mapped_column(Integer, nullable=False, default=1) enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True) # Optional admin-only action: run an Ansible playbook on transition-to-firing. # Shape: {source, playbook, inventory, extra_vars, limit, tags, check}. NULL = none. ansible_action: Mapped[dict | None] = mapped_column(JSON, nullable=True) created_by: Mapped[str] = mapped_column(String(36), ForeignKey("users.id", ondelete="RESTRICT"), nullable=False) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc) ) class AlertState(Base): __tablename__ = "alert_states" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4())) rule_id: Mapped[str] = mapped_column( String(36), ForeignKey("alert_rules.id", ondelete="CASCADE"), nullable=False, unique=True ) state: Mapped[AlertStateEnum] = mapped_column( Enum(AlertStateEnum), nullable=False, default=AlertStateEnum.inactive ) consecutive_failure_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0) last_transition_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc) ) acknowledged_by: Mapped[str | None] = mapped_column( String(36), ForeignKey("users.id", ondelete="SET NULL"), nullable=True ) acknowledged_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) class MaintenanceWindow(Base): __tablename__ = "maintenance_windows" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4())) name: Mapped[str] = mapped_column(String(128), nullable=False) start_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) end_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) # Scope — null means "all". scope_resource requires scope_source to be set. scope_source: Mapped[str | None] = mapped_column(String(64), nullable=True) scope_resource: Mapped[str | None] = mapped_column(String(255), nullable=True) created_by: Mapped[str] = mapped_column( String(36), ForeignKey("users.id", ondelete="RESTRICT"), nullable=False ) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc) ) class AlertEvent(Base): __tablename__ = "alert_events" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4())) rule_id: Mapped[str] = mapped_column( String(36), ForeignKey("alert_rules.id", ondelete="CASCADE"), nullable=False ) from_state: Mapped[str] = mapped_column(String(32), nullable=False) to_state: Mapped[str] = mapped_column(String(32), nullable=False) metric_value: Mapped[float] = mapped_column(Float, nullable=False) transitioned_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc) ) email_sent: Mapped[bool | None] = mapped_column(Boolean, nullable=True) email_error: Mapped[str | None] = mapped_column(Text, nullable=True) webhook_sent: Mapped[bool | None] = mapped_column(Boolean, nullable=True) webhook_error: Mapped[str | None] = mapped_column(Text, nullable=True) # Set when this firing event triggered an Ansible action run (audit link). ansible_run_id: Mapped[str | None] = mapped_column( String(36), ForeignKey("ansible_runs.id", ondelete="SET NULL"), nullable=True )