from fastapi import APIRouter, Depends, HTTPException from sqlalchemy import func, select from sqlalchemy.orm import Session from app.api.deps import CurrentUser from app.api.v1 import auth from app.db.session import get_db from app.models.domain import ( AuditLog, Cluster, Job, Network, Node, Policy, Project, SecurityGroup, Subnet, Tenant, User, Workload, ) from app.schemas.domain import ( AuditLogRead, ClusterCreate, ClusterRead, FirewallPreview, IpAddressRead, IpReservationCreate, JobRead, NetworkRead, NodeRead, PolicyCreate, PolicyRead, ProjectRead, SecurityGroupCreate, SecurityGroupRead, SubnetRead, TenantRead, UserRead, WorkloadRead, ) from app.services.audit import write_audit from app.services.firewall_orchestrator import FirewallOrchestrator from app.services.providers.base import ProviderConnection from app.services.providers.registry import get_provider api_router = APIRouter() api_router.include_router(auth.router) @api_router.get("/dashboard") def dashboard(_: CurrentUser, db: Session = Depends(get_db)) -> dict: return { "clusters": db.scalar(select(func.count()).select_from(Cluster)), "nodes": db.scalar(select(func.count()).select_from(Node)), "workloads": db.scalar(select(func.count()).select_from(Workload)), "networks": db.scalar(select(func.count()).select_from(Network)), "open_policy_violations": 1, "last_syncs": db.scalars(select(Cluster).order_by(Cluster.updated_at.desc()).limit(5)).all(), "faulty_nodes": db.scalars(select(Node).where(Node.status != "online")).all(), "top_talkers": [ {"name": "finance-app-2", "bytes": 942000000}, {"name": "core-services-1", "bytes": 512000000}, ], } @api_router.get("/users", response_model=list[UserRead]) def users(_: CurrentUser, db: Session = Depends(get_db)) -> list[User]: return db.scalars(select(User).order_by(User.email)).all() @api_router.get("/clusters", response_model=list[ClusterRead]) def clusters(_: CurrentUser, db: Session = Depends(get_db)) -> list[Cluster]: return db.scalars(select(Cluster).order_by(Cluster.name)).all() @api_router.post("/clusters", response_model=ClusterRead) def create_cluster(payload: ClusterCreate, user: CurrentUser, db: Session = Depends(get_db)) -> Cluster: cluster = Cluster( name=payload.name, api_url=payload.api_url, token_ref=payload.api_token, mode=payload.mode, verify_tls=payload.verify_tls, ) db.add(cluster) db.commit() db.refresh(cluster) write_audit(db, action="cluster.created", object_type="cluster", object_id=cluster.id, user_id=user.id) return cluster @api_router.post("/clusters/{cluster_id}/test") def test_cluster(cluster_id: str, _: CurrentUser, db: Session = Depends(get_db)) -> dict: cluster = db.get(Cluster, cluster_id) if not cluster: raise HTTPException(status_code=404, detail="Cluster not found") return {"cluster_id": cluster.id, "status": "configured", "message": "Provider connection is ready for live token validation."} @api_router.post("/clusters/{cluster_id}/sync") async def sync_cluster(cluster_id: str, user: CurrentUser, db: Session = Depends(get_db)) -> dict: cluster = db.get(Cluster, cluster_id) if not cluster: raise HTTPException(status_code=404, detail="Cluster not found") provider = get_provider(cluster.provider) inventory = await provider.sync_inventory( ProviderConnection( api_url=cluster.api_url, token=cluster.token_ref or "", verify_tls=cluster.verify_tls, read_only=cluster.mode == "read_only", ) ) cluster.last_sync_status = "success" cluster.last_sync_error = None db.add(Job(kind="proxmox.sync", status="success", progress=100, logs=[f"Synced {cluster.name}"])) db.commit() write_audit(db, action="cluster.sync", object_type="cluster", object_id=cluster.id, user_id=user.id) return {"cluster_id": cluster.id, "status": "success", "inventory_counts": {key: len(value) for key, value in inventory.items()}} @api_router.get("/nodes", response_model=list[NodeRead]) def nodes(_: CurrentUser, db: Session = Depends(get_db)) -> list[Node]: return db.scalars(select(Node).order_by(Node.name)).all() @api_router.get("/vms", response_model=list[WorkloadRead]) def workloads(_: CurrentUser, db: Session = Depends(get_db)) -> list[Workload]: return db.scalars(select(Workload).order_by(Workload.name)).all() @api_router.get("/networks", response_model=list[NetworkRead]) def networks(_: CurrentUser, db: Session = Depends(get_db)) -> list[Network]: return db.scalars(select(Network).order_by(Network.name)).all() @api_router.get("/ipam/subnets", response_model=list[SubnetRead]) def subnets(_: CurrentUser, db: Session = Depends(get_db)) -> list[Subnet]: return db.scalars(select(Subnet).order_by(Subnet.cidr)).all() @api_router.get("/ipam/addresses", response_model=list[IpAddressRead]) def ipam_addresses(_: CurrentUser, db: Session = Depends(get_db)): from app.models.domain import IpAddress return db.scalars(select(IpAddress).order_by(IpAddress.address)).all() @api_router.post("/ipam/addresses", response_model=IpAddressRead) def reserve_ip(payload: IpReservationCreate, user: CurrentUser, db: Session = Depends(get_db)): from app.models.domain import IpAddress address = IpAddress(subnet_id=payload.subnet_id, address=payload.address, status=payload.status, note=payload.note) db.add(address) db.commit() db.refresh(address) write_audit(db, action="ipam.address.created", object_type="ip_address", object_id=address.id, user_id=user.id) return address @api_router.get("/tenants", response_model=list[TenantRead]) def tenants(_: CurrentUser, db: Session = Depends(get_db)) -> list[Tenant]: return db.scalars(select(Tenant).order_by(Tenant.name)).all() @api_router.get("/projects", response_model=list[ProjectRead]) def projects(_: CurrentUser, db: Session = Depends(get_db)) -> list[Project]: return db.scalars(select(Project).order_by(Project.name)).all() @api_router.get("/security-groups", response_model=list[SecurityGroupRead]) def security_groups(_: CurrentUser, db: Session = Depends(get_db)) -> list[SecurityGroup]: return db.scalars(select(SecurityGroup).order_by(SecurityGroup.name)).all() @api_router.post("/security-groups", response_model=SecurityGroupRead) def create_security_group(payload: SecurityGroupCreate, user: CurrentUser, db: Session = Depends(get_db)) -> SecurityGroup: group = SecurityGroup(project_id=payload.project_id, name=payload.name, description=payload.description) db.add(group) db.commit() db.refresh(group) write_audit(db, action="security_group.created", object_type="security_group", object_id=group.id, user_id=user.id) return group @api_router.get("/policies", response_model=list[PolicyRead]) def policies(_: CurrentUser, db: Session = Depends(get_db)) -> list[Policy]: return db.scalars(select(Policy).order_by(Policy.name)).all() @api_router.post("/policies", response_model=PolicyRead) def create_policy(payload: PolicyCreate, user: CurrentUser, db: Session = Depends(get_db)) -> Policy: policy = Policy(project_id=payload.project_id, name=payload.name, enabled=payload.enabled, definition=payload.definition) db.add(policy) db.commit() db.refresh(policy) write_audit(db, action="policy.created", object_type="policy", object_id=policy.id, user_id=user.id) return policy @api_router.post("/firewall/preview/{policy_id}", response_model=FirewallPreview) async def firewall_preview(policy_id: str, user: CurrentUser, db: Session = Depends(get_db)) -> FirewallPreview: policy = db.get(Policy, policy_id) cluster = db.scalar(select(Cluster).order_by(Cluster.name).limit(1)) if not policy or not cluster: raise HTTPException(status_code=404, detail="Policy or cluster not found") preview = await FirewallOrchestrator().preview(cluster, policy) write_audit(db, action="firewall.preview", object_type="policy", object_id=policy.id, user_id=user.id, new_values=preview.model_dump()) return preview @api_router.get("/jobs", response_model=list[JobRead]) def jobs(_: CurrentUser, db: Session = Depends(get_db)) -> list[Job]: return db.scalars(select(Job).order_by(Job.created_at.desc())).all() @api_router.get("/audit", response_model=list[AuditLogRead]) def audit(_: CurrentUser, db: Session = Depends(get_db)) -> list[AuditLog]: return db.scalars(select(AuditLog).order_by(AuditLog.created_at.desc()).limit(200)).all() @api_router.get("/settings") def settings(_: CurrentUser) -> dict: return {"product": "NexaFabric", "firewall_apply_requires_preview": True, "agent_optional": True}