from datetime import datetime from enum import StrEnum from uuid import uuid4 from sqlalchemy import JSON, BigInteger, Boolean, DateTime, Enum, ForeignKey, Index, Integer, String, Text, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column, relationship from app.db.session import Base def new_id() -> str: return str(uuid4()) class ClusterMode(StrEnum): read_only = "read_only" write_enabled = "write_enabled" class JobStatus(StrEnum): queued = "queued" running = "running" success = "success" failed = "failed" cancelled = "cancelled" class IpStatus(StrEnum): free = "free" reserved = "reserved" assigned = "assigned" deprecated = "deprecated" conflict = "conflict" class RuleAction(StrEnum): allow = "allow" deny = "deny" reject = "reject" class Direction(StrEnum): ingress = "ingress" egress = "egress" class TimestampMixin: created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) class SystemSetting(Base, TimestampMixin): __tablename__ = "system_settings" key: Mapped[str] = mapped_column(String(100), primary_key=True) value: Mapped[dict] = mapped_column(JSON, default=dict) class User(Base, TimestampMixin): __tablename__ = "users" id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) email: Mapped[str] = mapped_column(String(255), unique=True, index=True) display_name: Mapped[str] = mapped_column(String(255)) password_hash: Mapped[str] = mapped_column(String(512)) is_active: Mapped[bool] = mapped_column(Boolean, default=True) failed_login_attempts: Mapped[int] = mapped_column(Integer, default=0) roles: Mapped[list["Role"]] = relationship(secondary="user_roles", back_populates="users") class Role(Base, TimestampMixin): __tablename__ = "roles" id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) name: Mapped[str] = mapped_column(String(100), unique=True) permissions: Mapped[list[str]] = mapped_column(JSON, default=list) users: Mapped[list[User]] = relationship(secondary="user_roles", back_populates="roles") class UserRole(Base): __tablename__ = "user_roles" user_id: Mapped[str] = mapped_column(ForeignKey("users.id"), primary_key=True) role_id: Mapped[str] = mapped_column(ForeignKey("roles.id"), primary_key=True) class Tenant(Base, TimestampMixin): __tablename__ = "tenants" id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) name: Mapped[str] = mapped_column(String(255), unique=True) description: Mapped[str | None] = mapped_column(Text) class Project(Base, TimestampMixin): __tablename__ = "projects" id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) tenant_id: Mapped[str] = mapped_column(ForeignKey("tenants.id"), index=True) name: Mapped[str] = mapped_column(String(255)) description: Mapped[str | None] = mapped_column(Text) tenant: Mapped[Tenant] = relationship() class Cluster(Base, TimestampMixin): __tablename__ = "clusters" id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) name: Mapped[str] = mapped_column(String(255), unique=True) api_url: Mapped[str] = mapped_column(String(512)) provider: Mapped[str] = mapped_column(String(100), default="proxmox") mode: Mapped[ClusterMode] = mapped_column(Enum(ClusterMode), default=ClusterMode.read_only) token_ref: Mapped[str | None] = mapped_column(String(512)) verify_tls: Mapped[bool] = mapped_column(Boolean, default=True) last_sync_at: Mapped[datetime | None] = mapped_column(DateTime) last_sync_status: Mapped[str | None] = mapped_column(String(100)) last_sync_error: Mapped[str | None] = mapped_column(Text) class Node(Base, TimestampMixin): __tablename__ = "nodes" id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) cluster_id: Mapped[str] = mapped_column(ForeignKey("clusters.id"), index=True) name: Mapped[str] = mapped_column(String(255)) status: Mapped[str] = mapped_column(String(100), default="unknown") cpu_count: Mapped[int] = mapped_column(Integer, default=0) memory_mb: Mapped[int] = mapped_column(Integer, default=0) cluster: Mapped[Cluster] = relationship() class NodeAgent(Base, TimestampMixin): __tablename__ = "node_agents" node_id: Mapped[str] = mapped_column(ForeignKey("nodes.id"), primary_key=True) status: Mapped[str] = mapped_column(String(100), default="not_installed") version: Mapped[str | None] = mapped_column(String(50)) last_seen_at: Mapped[datetime | None] = mapped_column(DateTime) last_payload: Mapped[dict | None] = mapped_column(JSON) install_count: Mapped[int] = mapped_column(Integer, default=0) node: Mapped[Node] = relationship() class Workload(Base, TimestampMixin): __tablename__ = "workloads" id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) cluster_id: Mapped[str] = mapped_column(ForeignKey("clusters.id"), index=True) node_id: Mapped[str] = mapped_column(ForeignKey("nodes.id"), index=True) project_id: Mapped[str | None] = mapped_column(ForeignKey("projects.id"), index=True) external_id: Mapped[str] = mapped_column(String(100)) name: Mapped[str] = mapped_column(String(255)) kind: Mapped[str] = mapped_column(String(50)) status: Mapped[str] = mapped_column(String(100), default="unknown") tags: Mapped[list[str]] = mapped_column(JSON, default=list) class Network(Base, TimestampMixin): __tablename__ = "networks" id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) cluster_id: Mapped[str] = mapped_column(ForeignKey("clusters.id"), index=True) project_id: Mapped[str | None] = mapped_column(ForeignKey("projects.id"), index=True) name: Mapped[str] = mapped_column(String(255)) kind: Mapped[str] = mapped_column(String(50)) vlan_id: Mapped[int | None] = mapped_column(Integer) mtu: Mapped[int] = mapped_column(Integer, default=1500) gateway: Mapped[str | None] = mapped_column(String(100)) dns: Mapped[list[str]] = mapped_column(JSON, default=list) dhcp_enabled: Mapped[bool] = mapped_column(Boolean, default=False) tags: Mapped[list[str]] = mapped_column(JSON, default=list) description: Mapped[str | None] = mapped_column(Text) class Subnet(Base, TimestampMixin): __tablename__ = "subnets" id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) network_id: Mapped[str] = mapped_column(ForeignKey("networks.id"), index=True) cidr: Mapped[str] = mapped_column(String(100)) gateway: Mapped[str | None] = mapped_column(String(100)) dns: Mapped[list[str]] = mapped_column(JSON, default=list) dhcp_enabled: Mapped[bool] = mapped_column(Boolean, default=False) class IpAddress(Base, TimestampMixin): __tablename__ = "ip_addresses" __table_args__ = ( UniqueConstraint("subnet_id", "address"), Index("ix_ip_addresses_address", "address"), ) id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) subnet_id: Mapped[str] = mapped_column(ForeignKey("subnets.id"), index=True) address: Mapped[str] = mapped_column(String(100)) status: Mapped[IpStatus] = mapped_column(Enum(IpStatus), default=IpStatus.free) workload_id: Mapped[str | None] = mapped_column(ForeignKey("workloads.id"), index=True) note: Mapped[str | None] = mapped_column(Text) class TrafficFlow(Base, TimestampMixin): __tablename__ = "traffic_flows" __table_args__ = ( Index("ix_traffic_flows_node_updated", "node_id", "updated_at"), Index("ix_traffic_flows_source_updated", "source_ip", "updated_at"), Index("ix_traffic_flows_destination_updated", "destination_ip", "updated_at"), Index("ix_traffic_flows_destination_port_updated", "destination_port", "updated_at"), Index("ix_traffic_flows_state_updated", "state", "updated_at"), ) id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) node_id: Mapped[str] = mapped_column(ForeignKey("nodes.id"), index=True) source_ip: Mapped[str] = mapped_column(String(100), index=True) destination_ip: Mapped[str] = mapped_column(String(100), index=True) protocol: Mapped[str] = mapped_column(String(20), default="unknown") source_port: Mapped[int | None] = mapped_column(Integer) destination_port: Mapped[int | None] = mapped_column(Integer) bytes: Mapped[int] = mapped_column(BigInteger, default=0) packets: Mapped[int] = mapped_column(BigInteger, default=0) state: Mapped[str | None] = mapped_column(String(100)) observed_at: Mapped[datetime | None] = mapped_column(DateTime) raw: Mapped[dict | None] = mapped_column(JSON) class SecurityGroup(Base, TimestampMixin): __tablename__ = "security_groups" id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) project_id: Mapped[str | None] = mapped_column(ForeignKey("projects.id"), index=True) name: Mapped[str] = mapped_column(String(255)) description: Mapped[str | None] = mapped_column(Text) class SecurityRule(Base, TimestampMixin): __tablename__ = "security_rules" id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) security_group_id: Mapped[str] = mapped_column(ForeignKey("security_groups.id"), index=True) direction: Mapped[Direction] = mapped_column(Enum(Direction)) action: Mapped[RuleAction] = mapped_column(Enum(RuleAction)) protocol: Mapped[str] = mapped_column(String(20), default="any") source: Mapped[str] = mapped_column(String(255), default="any") destination: Mapped[str] = mapped_column(String(255), default="any") port: Mapped[str | None] = mapped_column(String(100)) priority: Mapped[int] = mapped_column(Integer, default=1000) logging: Mapped[bool] = mapped_column(Boolean, default=False) description: Mapped[str | None] = mapped_column(Text) class Policy(Base, TimestampMixin): __tablename__ = "policies" id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) project_id: Mapped[str | None] = mapped_column(ForeignKey("projects.id"), index=True) name: Mapped[str] = mapped_column(String(255)) version: Mapped[int] = mapped_column(Integer, default=1) enabled: Mapped[bool] = mapped_column(Boolean, default=True) definition: Mapped[dict] = mapped_column(JSON, default=dict) last_compiled: Mapped[dict | None] = mapped_column(JSON) @property def enforcement_mode(self) -> str: return (self.definition or {}).get("enforcement_mode", "enforced") class ServiceCatalogItem(Base, TimestampMixin): __tablename__ = "service_catalog" id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) name: Mapped[str] = mapped_column(String(255), unique=True) protocol: Mapped[str] = mapped_column(String(20)) ports: Mapped[str] = mapped_column(String(100)) editable: Mapped[bool] = mapped_column(Boolean, default=True) class Job(Base, TimestampMixin): __tablename__ = "jobs" id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) kind: Mapped[str] = mapped_column(String(100)) status: Mapped[JobStatus] = mapped_column(Enum(JobStatus), default=JobStatus.queued) progress: Mapped[int] = mapped_column(Integer, default=0) started_at: Mapped[datetime | None] = mapped_column(DateTime) finished_at: Mapped[datetime | None] = mapped_column(DateTime) logs: Mapped[list[str]] = mapped_column(JSON, default=list) error: Mapped[str | None] = mapped_column(Text) class AuditLog(Base): __tablename__ = "audit_logs" id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id) created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, index=True) user_id: Mapped[str | None] = mapped_column(ForeignKey("users.id"), index=True) action: Mapped[str] = mapped_column(String(100), index=True) object_type: Mapped[str] = mapped_column(String(100), index=True) object_id: Mapped[str | None] = mapped_column(String(100), index=True) old_values: Mapped[dict | None] = mapped_column(JSON) new_values: Mapped[dict | None] = mapped_column(JSON) ip_address: Mapped[str | None] = mapped_column(String(100)) user_agent: Mapped[str | None] = mapped_column(String(512)) result: Mapped[str] = mapped_column(String(100), default="success") error_text: Mapped[str | None] = mapped_column(Text)