|
|
|
@@ -0,0 +1,226 @@
|
|
|
|
|
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}
|