Files
NexaFabric/backend/app/models/domain.py
T
nessi b9b0d4ae39 feat: add security group membership with workload assignment, sg: prefix resolution in policy rules, and searchable select component
Add SecurityGroupMember model with security_group_id/workload_id foreign keys and unique constraint, implement security_group_members table with timestamps, add SecurityGroupMemberCreate/SecurityGroupMemberRead schemas with workload_name/workload_external_id fields, implement workload_provider_targets helper to expand sg: prefix into multiple workload targets with
2026-07-10 12:46:56 +02:00

315 lines
13 KiB
Python

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 SecurityGroupMember(Base, TimestampMixin):
__tablename__ = "security_group_members"
__table_args__ = (UniqueConstraint("security_group_id", "workload_id"),)
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)
workload_id: Mapped[str] = mapped_column(ForeignKey("workloads.id"), index=True)
security_group: Mapped[SecurityGroup] = relationship()
workload: Mapped[Workload] = relationship()
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)