feat: add policy deployment status tracking with cluster-level rule state monitoring and live firewall rule version comparison

Add policy_read_payload helper to build policy response with deployment status, implement nexafabric_rule_version to parse policy ID and version from rule comments, add policy_deployment_status to check active/stale/partial/unresolved states by comparing expected rules from preview against live firewall rules per cluster with version matching, extend PolicyRead schema with
This commit is contained in:
2026-07-09 19:52:10 +02:00
parent 6d5dc310df
commit 1802c2cbee
4 changed files with 216 additions and 3 deletions
+158 -2
View File
@@ -526,6 +526,159 @@ def flow_policy_decision(active_matches: list[dict[str, object]], policy_matches
return "observed"
def policy_read_payload(policy: Policy, deployment_status: dict[str, object] | None = None) -> dict[str, object]:
return {
"id": policy.id,
"project_id": policy.project_id,
"name": policy.name,
"version": policy.version,
"enabled": policy.enabled,
"enforcement_mode": policy.enforcement_mode,
"definition": policy.definition,
"last_compiled": policy.last_compiled,
"deployment_status": deployment_status,
}
def nexafabric_rule_version(rule: dict[str, object], policy_id: str) -> int | None:
comment = str(rule.get("comment") or "")
marker = f"NexaFabric policy={policy_id} version="
if marker not in comment:
return None
try:
return int(comment.split(marker, 1)[1].split(" ", 1)[0])
except (IndexError, ValueError):
return None
async def policy_deployment_status(db: Session, policy: Policy) -> dict[str, object]:
if not policy.enabled:
return {"state": "disabled", "label": "Disabled", "expected_rules": 0, "active_rules": 0, "stale_rules": 0, "clusters": []}
if policy.enforcement_mode == "audit":
return {"state": "audit", "label": "Audit mode", "expected_rules": 0, "active_rules": 0, "stale_rules": 0, "clusters": []}
clusters = db.scalars(select(Cluster).order_by(Cluster.name)).all()
if not clusters:
return {"state": "unknown", "label": "No cluster", "expected_rules": 0, "active_rules": 0, "stale_rules": 0, "clusters": []}
expected_rules = 0
active_rules = 0
stale_rules = 0
unresolved = 0
cluster_results: list[dict[str, object]] = []
for cluster in clusters:
try:
preview = resolve_firewall_preview(db, cluster, await FirewallOrchestrator().preview(cluster, policy))
except Exception as exc:
cluster_results.append({"cluster_id": cluster.id, "cluster_name": cluster.name, "state": "error", "error": str(exc)})
continue
writable_rules = [
rule for rule in preview.generated_rules if rule.get("provider_target") and rule.get("provider_rule") and not rule.get("audit_only")
]
expected_rules += len(writable_rules)
unresolved += len(preview.conflicts)
provider = get_provider(cluster.provider)
list_rules = getattr(provider, "list_firewall_rules", None)
if not list_rules:
cluster_results.append(
{
"cluster_id": cluster.id,
"cluster_name": cluster.name,
"state": "unknown",
"expected_rules": len(writable_rules),
"active_rules": 0,
"stale_rules": 0,
"reason": "Provider cannot list active firewall rules.",
}
)
continue
cluster_active = 0
cluster_stale = 0
seen_targets: dict[str, dict[str, object]] = {}
for rule in writable_rules:
target = rule.get("provider_target")
if isinstance(target, dict):
seen_targets[str(target)] = target
for target in seen_targets.values():
try:
rules = await list_rules(
ProviderConnection(
api_url=cluster.api_url,
token=cluster.token_ref or "",
verify_tls=cluster.verify_tls,
read_only=True,
),
target,
)
except Exception as exc:
cluster_results.append({"cluster_id": cluster.id, "cluster_name": cluster.name, "state": "error", "error": str(exc)})
continue
for active_rule in rules:
version = nexafabric_rule_version(active_rule, policy.id)
if version is None:
continue
if version == policy.version:
cluster_active += 1
else:
cluster_stale += 1
active_rules += cluster_active
stale_rules += cluster_stale
if preview.conflicts:
cluster_state = "unresolved"
elif cluster_active >= len(writable_rules) and writable_rules:
cluster_state = "active"
elif cluster_active:
cluster_state = "partial"
elif cluster_stale:
cluster_state = "stale"
else:
cluster_state = "not_applied"
cluster_results.append(
{
"cluster_id": cluster.id,
"cluster_name": cluster.name,
"state": cluster_state,
"expected_rules": len(writable_rules),
"active_rules": cluster_active,
"stale_rules": cluster_stale,
"conflicts": preview.conflicts,
}
)
if unresolved:
state = "unresolved"
label = "Needs attention"
elif expected_rules and active_rules >= expected_rules:
state = "active"
label = "Active"
elif active_rules:
state = "partial"
label = "Partially active"
elif stale_rules:
state = "stale"
label = "Outdated"
elif expected_rules:
state = "not_applied"
label = "Not applied"
else:
state = "unknown"
label = "No resolved rules"
return {
"state": state,
"label": label,
"expected_rules": expected_rules,
"active_rules": active_rules,
"stale_rules": stale_rules,
"clusters": cluster_results,
}
def dashboard_top_talkers(db: Session) -> list[dict[str, int | str]]:
totals: dict[str, int] = {}
workloads = db.scalars(select(Workload)).all()
@@ -1593,8 +1746,11 @@ def delete_security_rule(rule_id: str, user: CurrentUser, db: Session = Depends(
@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()
async def policies(_: CurrentUser, db: Session = Depends(get_db)) -> list[dict[str, object]]:
return [
policy_read_payload(policy, await policy_deployment_status(db, policy))
for policy in db.scalars(select(Policy).order_by(Policy.name)).all()
]
@api_router.post("/policies", response_model=PolicyRead)