feat: add cluster update/delete endpoints, expand tcp/udp protocol handling, and enhance cluster management UI
Add PATCH /clusters/{cluster_id} endpoint with optional token update and audit logging, implement DELETE /clusters/{cluster_id} with cascading deletion of nodes, workloads, networks, subnets, and IP addresses, expand firewall rule generation to split tcp/udp protocol into separate tcp and udp rules for Proxmox compatibility, add ClusterUpdate schema with optional api_token field, include
This commit is contained in:
@@ -36,6 +36,7 @@ from app.schemas.domain import (
|
||||
AuditLogRead,
|
||||
ClusterCreate,
|
||||
ClusterRead,
|
||||
ClusterUpdate,
|
||||
FirewallApplyRequest,
|
||||
FirewallPreview,
|
||||
IpAddressRead,
|
||||
@@ -264,26 +265,28 @@ def resolve_firewall_preview(db: Session, cluster: Cluster, preview: FirewallPre
|
||||
|
||||
ports = str(rule.get("ports", "any"))
|
||||
protocol = str(rule.get("protocol", "any"))
|
||||
protocols = ["tcp", "udp"] if protocol == "tcp/udp" else [protocol]
|
||||
for remote_value in remote_values:
|
||||
provider_rule = {
|
||||
"type": "in" if direction == "ingress" else "out",
|
||||
"action": proxmox_action(str(rule.get("action", "allow"))),
|
||||
"enable": 1,
|
||||
"comment": (
|
||||
f"NexaFabric policy={rule.get('policy_id')} version={rule.get('policy_version')} "
|
||||
f"rule={rule_index} target={target_workload.name}"
|
||||
),
|
||||
}
|
||||
if protocol != "any":
|
||||
provider_rule["proto"] = protocol
|
||||
if ports != "any":
|
||||
provider_rule["dport"] = ports
|
||||
if remote_value:
|
||||
provider_rule["source" if direction == "ingress" else "dest"] = remote_value
|
||||
if rule.get("logging"):
|
||||
provider_rule["log"] = "info"
|
||||
mapped_rule = {**mapped, "provider_target": provider_target, "provider_rule": provider_rule}
|
||||
generated_rules.append(mapped_rule)
|
||||
for provider_protocol in protocols:
|
||||
provider_rule = {
|
||||
"type": "in" if direction == "ingress" else "out",
|
||||
"action": proxmox_action(str(rule.get("action", "allow"))),
|
||||
"enable": 1,
|
||||
"comment": (
|
||||
f"NexaFabric policy={rule.get('policy_id')} version={rule.get('policy_version')} "
|
||||
f"rule={rule_index} target={target_workload.name}"
|
||||
),
|
||||
}
|
||||
if provider_protocol != "any":
|
||||
provider_rule["proto"] = provider_protocol
|
||||
if ports != "any":
|
||||
provider_rule["dport"] = ports
|
||||
if remote_value:
|
||||
provider_rule["source" if direction == "ingress" else "dest"] = remote_value
|
||||
if rule.get("logging"):
|
||||
provider_rule["log"] = "info"
|
||||
mapped_rule = {**mapped, "provider_target": provider_target, "provider_rule": provider_rule}
|
||||
generated_rules.append(mapped_rule)
|
||||
|
||||
return FirewallPreview(
|
||||
policy_id=preview.policy_id,
|
||||
@@ -434,6 +437,68 @@ def create_cluster(payload: ClusterCreate, user: CurrentUser, db: Session = Depe
|
||||
return cluster
|
||||
|
||||
|
||||
@api_router.patch("/clusters/{cluster_id}", response_model=ClusterRead)
|
||||
def update_cluster(cluster_id: str, payload: ClusterUpdate, user: CurrentUser, db: Session = Depends(get_db)) -> Cluster:
|
||||
cluster = db.get(Cluster, cluster_id)
|
||||
if not cluster:
|
||||
raise HTTPException(status_code=404, detail="Cluster not found")
|
||||
old_values = {
|
||||
"name": cluster.name,
|
||||
"api_url": cluster.api_url,
|
||||
"provider": cluster.provider,
|
||||
"mode": cluster.mode,
|
||||
"verify_tls": cluster.verify_tls,
|
||||
}
|
||||
cluster.name = payload.name
|
||||
cluster.api_url = payload.api_url
|
||||
cluster.provider = payload.provider
|
||||
cluster.mode = payload.mode
|
||||
cluster.verify_tls = payload.verify_tls
|
||||
if payload.api_token:
|
||||
cluster.token_ref = payload.api_token
|
||||
commit_or_400(db)
|
||||
db.refresh(cluster)
|
||||
write_audit(
|
||||
db,
|
||||
action="cluster.updated",
|
||||
object_type="cluster",
|
||||
object_id=cluster.id,
|
||||
user_id=user.id,
|
||||
old_values=old_values,
|
||||
new_values={**payload.model_dump(exclude={"api_token"}), "api_token_changed": bool(payload.api_token)},
|
||||
)
|
||||
return cluster
|
||||
|
||||
|
||||
@api_router.delete("/clusters/{cluster_id}")
|
||||
def delete_cluster(cluster_id: str, user: CurrentUser, db: Session = Depends(get_db)) -> dict[str, str]:
|
||||
cluster = db.get(Cluster, cluster_id)
|
||||
if not cluster:
|
||||
raise HTTPException(status_code=404, detail="Cluster not found")
|
||||
workload_ids = [row[0] for row in db.execute(select(Workload.id).where(Workload.cluster_id == cluster.id)).all()]
|
||||
if workload_ids:
|
||||
for address in db.scalars(select(IpAddress).where(IpAddress.workload_id.in_(workload_ids))).all():
|
||||
db.delete(address)
|
||||
network_ids = [row[0] for row in db.execute(select(Network.id).where(Network.cluster_id == cluster.id)).all()]
|
||||
if network_ids:
|
||||
subnet_ids = [row[0] for row in db.execute(select(Subnet.id).where(Subnet.network_id.in_(network_ids))).all()]
|
||||
if subnet_ids:
|
||||
for address in db.scalars(select(IpAddress).where(IpAddress.subnet_id.in_(subnet_ids))).all():
|
||||
db.delete(address)
|
||||
for subnet in db.scalars(select(Subnet).where(Subnet.id.in_(subnet_ids))).all():
|
||||
db.delete(subnet)
|
||||
for network in db.scalars(select(Network).where(Network.id.in_(network_ids))).all():
|
||||
db.delete(network)
|
||||
for workload in db.scalars(select(Workload).where(Workload.cluster_id == cluster.id)).all():
|
||||
db.delete(workload)
|
||||
for node in db.scalars(select(Node).where(Node.cluster_id == cluster.id)).all():
|
||||
db.delete(node)
|
||||
db.delete(cluster)
|
||||
commit_or_400(db)
|
||||
write_audit(db, action="cluster.deleted", object_type="cluster", object_id=cluster_id, user_id=user.id)
|
||||
return {"status": "deleted", "id": cluster_id}
|
||||
|
||||
|
||||
@api_router.post("/clusters/{cluster_id}/test")
|
||||
async def test_cluster(cluster_id: str, _: CurrentUser, db: Session = Depends(get_db)) -> dict:
|
||||
cluster = db.get(Cluster, cluster_id)
|
||||
|
||||
@@ -71,6 +71,15 @@ class ClusterCreate(BaseModel):
|
||||
verify_tls: bool = True
|
||||
|
||||
|
||||
class ClusterUpdate(BaseModel):
|
||||
name: str
|
||||
api_url: str
|
||||
api_token: str | None = Field(default=None, min_length=8)
|
||||
provider: str = "proxmox"
|
||||
mode: str = "read_only"
|
||||
verify_tls: bool = True
|
||||
|
||||
|
||||
class TenantCreate(BaseModel):
|
||||
name: str
|
||||
description: str | None = None
|
||||
@@ -157,6 +166,7 @@ class ClusterRead(OrmModel):
|
||||
api_url: str
|
||||
provider: str
|
||||
mode: str
|
||||
verify_tls: bool
|
||||
last_sync_at: datetime | None
|
||||
last_sync_status: str | None
|
||||
last_sync_error: str | None
|
||||
|
||||
Reference in New Issue
Block a user