chore: initial project setup with backend, frontend, CI/CD, and documentation
Add complete NexaFabric project structure including: - FastAPI backend with SQLAlchemy models, JWT auth, RBAC, audit logging, and provider interfaces - React + TypeScript frontend with Vite, Tailwind CSS, TanStack Query, and Zustand - Docker Compose configuration for PostgreSQL, Redis, API, worker, frontend, and nginx - GitHub Actions and GitLab CI workflows for testing, linting, building, and security scanning - Environment
This commit is contained in:
@@ -0,0 +1,254 @@
|
||||
from datetime import datetime
|
||||
from enum import StrEnum
|
||||
from uuid import uuid4
|
||||
|
||||
from sqlalchemy import JSON, Boolean, DateTime, Enum, ForeignKey, 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 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 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"),)
|
||||
|
||||
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 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)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user