Files
haproxy-openmanager/backend/routers/dashboard.py
T
taylanbakircioglu 9e5185c458 fix(security): post-review hardening — agent-inventory regression, coverage gaps, SSRF newNonce
Follow-up to the RCE/missing-auth/SSRF remediation, from a thorough multi-lens
review (3 agents + a black-box audit of all 201 routes). Backend-only; no
agent-script changes.

Regression fix (introduced by the previous commit):
- GET /api/agents was made JWT-only, but deployed agents call it WITH X-API-Key
  (not a JWT) to read their applied_config_version and avoid re-applying config on
  restart. It now accepts EITHER a valid operator JWT OR a valid agent X-API-Key,
  so agents no longer get 401 (which caused a spurious HAProxy reload every restart).

Completeness (GHSA-3p5c siblings the first pass missed — same data class, now JWT):
- dashboard.py: GET /api/haproxy-cluster-pools/{id}/agents (full agent inventory —
  a direct anonymous bypass of the GET /api/agents lockdown), /api/pools,
  /api/haproxy-cluster-pools, /api/dashboard/stats, /api/dashboard/overview
  (auth was optional -> leaked stats/names/health/alerts anonymously),
  /api/haproxy/stats.
- waf.py: GET /api/waf/rules. health.py: GET /api/health/errors.
- agent.py: GET /api/agents/generate-uninstall-script/{platform} (agent-management
  endpoint; was anonymous) now requires JWT or agent key, like generate-install-script.
- config.py: POST /api/config/{validate,optimize,templates/{id}/generate} were
  optional-auth (logging only) and run a HAProxy validator on caller input; now
  require a JWT. (bulk-create, parse-bulk, diff and configuration/request were
  already mandatory-auth — verified.)
  All newly-gated endpoints are frontend-only (axios sends the JWT) or unused;
  agents never call them.

SSRF (GHSA-3vh4) gap:
- acme_service._get_nonce fetched directory['newNonce'] (from the attacker-
  influenceable directory JSON) with a bare session, http allowed, dual-stack, and
  BEFORE the guarded _signed_request POST. Now guarded (assert_public_url +
  safe_connector + no redirects + timeout), matching the other ACME sinks.

Correctness:
- Three agent webhooks (config-applied, config-validation-failed, config-sync)
  swallowed their auth 401 into a 200 error body via a bare `except Exception`.
  Added `except HTTPException: raise` so the 401/403 propagates.

Audit result (live black-box, all 201 routes probed unauthenticated): no data
leak and no unauthenticated mutation anywhere; every sensitive route returns
401/403 (a pre-existing group of read handlers wraps the 401 into a 500 via a
broad except — no data is exposed; left as-is, documented as cosmetic).

Verified: full pytest tests/ (1145 passed, 0 failed; +16 regression tests) + live
localtest stack smoke — agent-key GET /api/agents=200, anonymous=401, all newly
gated endpoints reject anonymous and admit JWT, the 3 webhooks return 401.
2026-07-20 13:43:32 +03:00

792 lines
34 KiB
Python

from fastapi import APIRouter, HTTPException, Header, Depends
from auth_middleware import require_authenticated_user
from typing import Optional
from datetime import datetime
import logging
from models import PoolCreate, PoolUpdate
from database.connection import get_database_connection, close_database_connection
from utils.activity_log import log_user_activity
from agent_notifications import get_cluster_agents_status
router = APIRouter(prefix="/api", tags=["dashboard", "pools"])
logger = logging.getLogger(__name__)
@router.get("/dashboard/overview", dependencies=[Depends(require_authenticated_user)]) # SECURITY (GHSA-3p5c): leaked cluster/pool/agent stats, names, health & alerts anonymously (auth was optional)
async def get_dashboard_overview(cluster_id: Optional[int] = None, authorization: str = Header(None)):
"""Get dashboard overview with comprehensive statistics, optionally filtered by cluster"""
try:
# Get current user for cluster validation
from auth_middleware import get_current_user_from_token
current_user = await get_current_user_from_token(authorization) if authorization else None
conn = await get_database_connection()
# Validate cluster access if cluster_id provided
if cluster_id and current_user:
cluster_exists = await conn.fetchval("SELECT id FROM haproxy_clusters WHERE id = $1", cluster_id)
if not cluster_exists:
raise HTTPException(status_code=404, detail="Cluster not found")
# Build cluster filter condition
cluster_filter = ""
cluster_params = []
if cluster_id:
cluster_filter = " WHERE cluster_id = $1"
cluster_params = [cluster_id]
# Get basic dashboard statistics (safer queries with fallbacks)
try:
# Basic counts with error handling
if cluster_id:
clusters_count = 1 # Single cluster view
active_clusters = await conn.fetchval("SELECT COUNT(*) FROM haproxy_clusters WHERE id = $1 AND is_active = TRUE", cluster_id) or 0
else:
clusters_count = await conn.fetchval("SELECT COUNT(*) FROM haproxy_clusters") or 0
active_clusters = await conn.fetchval("SELECT COUNT(*) FROM haproxy_clusters WHERE is_active = TRUE") or 0
pools_count = await conn.fetchval("SELECT COUNT(*) FROM haproxy_cluster_pools") or 0
active_pools = await conn.fetchval("SELECT COUNT(*) FROM haproxy_cluster_pools WHERE is_active = TRUE") or 0
agents_count = await conn.fetchval("SELECT COUNT(*) FROM agents") or 0
online_agents = await conn.fetchval("SELECT COUNT(*) FROM agents WHERE status = 'online'") or 0
# Try to get frontend/backend counts, fallback to 0 if tables don't exist
try:
frontends_count = await conn.fetchval(f"SELECT COUNT(*) FROM frontends{cluster_filter}", *cluster_params) or 0
active_frontends = await conn.fetchval(f"SELECT COUNT(*) FROM frontends WHERE is_active = TRUE{' AND cluster_id = $1' if cluster_id else ''}", *cluster_params) or 0
except:
frontends_count = 0
active_frontends = 0
try:
backends_count = await conn.fetchval(f"SELECT COUNT(*) FROM backends{cluster_filter}", *cluster_params) or 0
active_backends = await conn.fetchval(f"SELECT COUNT(*) FROM backends WHERE is_active = TRUE{' AND cluster_id = $1' if cluster_id else ''}", *cluster_params) or 0
except:
backends_count = 0
active_backends = 0
try:
servers_count = await conn.fetchval("SELECT COUNT(*) FROM backend_servers") or 0
except:
servers_count = 0
try:
waf_rules_count = await conn.fetchval("SELECT COUNT(*) FROM waf_rules") or 0
except:
waf_rules_count = 0
try:
ssl_certs_count = await conn.fetchval("SELECT COUNT(*) FROM ssl_certificates") or 0
except:
ssl_certs_count = 0
try:
pending_configs = await conn.fetchval("SELECT COUNT(*) FROM config_versions WHERE status = 'PENDING'") or 0
active_configs = await conn.fetchval("SELECT COUNT(*) FROM config_versions WHERE status = 'APPLIED' AND is_active = TRUE") or 0
except:
# Fallback for older schema without status column
try:
pending_configs = 0
active_configs = await conn.fetchval("SELECT COUNT(*) FROM config_versions WHERE is_active = TRUE") or 0
except:
pending_configs = 0
active_configs = 0
stats = {
'total_clusters': clusters_count,
'active_clusters': active_clusters,
'total_pools': pools_count,
'active_pools': active_pools,
'total_agents': agents_count,
'online_agents': online_agents,
'offline_agents': agents_count - online_agents,
'warning_agents': 0, # Simplified for now
'total_frontends': frontends_count,
'active_frontends': active_frontends,
'total_backends': backends_count,
'active_backends': active_backends,
'total_servers': servers_count,
'total_waf_rules': waf_rules_count,
'total_ssl_certs': ssl_certs_count,
'pending_configs': pending_configs,
'active_configs': active_configs
}
except Exception as stats_error:
logger.error(f"Error fetching basic stats: {stats_error}")
# Return empty stats as fallback
stats = {
'total_clusters': 0, 'active_clusters': 0,
'total_pools': 0, 'active_pools': 0,
'total_agents': 0, 'online_agents': 0, 'offline_agents': 0, 'warning_agents': 0,
'total_frontends': 0, 'active_frontends': 0,
'total_backends': 0, 'active_backends': 0,
'total_servers': 0, 'total_waf_rules': 0, 'total_ssl_certs': 0,
'pending_configs': 0, 'active_configs': 0
}
# Get recent activity (simplified with error handling)
recent_activity = []
try:
recent_activity = await conn.fetch("""
SELECT resource_type, action, COUNT(*) as count
FROM user_activity_logs
WHERE created_at >= CURRENT_TIMESTAMP - INTERVAL '24 hours'
GROUP BY resource_type, action
ORDER BY count DESC
LIMIT 10
""")
except Exception as activity_error:
logger.warning(f"Could not fetch recent activity: {activity_error}")
recent_activity = []
# Get cluster health summary (simplified)
cluster_health = []
try:
cluster_health = await conn.fetch("""
SELECT
hc.id,
hc.name,
hc.connection_status,
hc.is_active
FROM haproxy_clusters hc
WHERE hc.is_active = TRUE
ORDER BY hc.name
""")
except Exception as cluster_error:
logger.warning(f"Could not fetch cluster health: {cluster_error}")
cluster_health = []
# Simplified alerts (only check basic agent status)
alerts = []
try:
offline_agents = await conn.fetch("""
SELECT name, last_seen
FROM agents
WHERE status = 'offline'
LIMIT 5
""")
for agent in offline_agents:
alerts.append({
"type": "warning",
"category": "agent",
"message": f"Agent '{agent['name']}' is offline",
"details": f"Last seen: {agent['last_seen']}" if agent['last_seen'] else "Never connected"
})
except Exception as alerts_error:
logger.warning(f"Could not fetch alerts: {alerts_error}")
alerts = []
await close_database_connection(conn)
return {
"overview": {
"clusters": {
"active": stats.get("active_clusters", 0),
"total": stats.get("total_clusters", 0)
},
"pools": {
"active": stats.get("active_pools", 0),
"total": stats.get("total_pools", 0)
},
"agents": {
"online": stats.get("online_agents", 0),
"offline": stats.get("offline_agents", 0),
"warning": stats.get("warning_agents", 0),
"total": stats.get("total_agents", 0)
},
"configurations": {
"frontends": {
"active": stats.get("active_frontends", 0),
"total": stats.get("total_frontends", 0)
},
"backends": {
"active": stats.get("active_backends", 0),
"total": stats.get("total_backends", 0)
},
"servers": stats.get("total_servers", 0),
"waf_rules": stats.get("total_waf_rules", 0),
"ssl_certificates": stats.get("total_ssl_certs", 0)
},
"config_versions": {
"pending": stats.get("pending_configs", 0),
"active": stats.get("active_configs", 0)
}
},
"recent_activity": [
{
"resource_type": activity["resource_type"],
"action": activity["action"],
"count": activity["count"]
}
for activity in recent_activity
],
"cluster_health": [
{
"id": cluster["id"],
"name": cluster["name"],
"status": cluster.get("connection_status", "unknown"),
"is_active": cluster.get("is_active", False)
}
for cluster in cluster_health
],
"alerts": alerts[:20] # Limit to 20 most important alerts
}
except Exception as e:
logger.error(f"Error fetching dashboard overview: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/dashboard/stats", dependencies=[Depends(require_authenticated_user)]) # SECURITY (GHSA-3p5c): aggregate cluster/agent counts
async def get_dashboard_stats():
"""Get dashboard statistics"""
try:
conn = await get_database_connection()
# Get basic counts
stats = await conn.fetchrow("""
SELECT
(SELECT COUNT(*) FROM haproxy_clusters WHERE is_active = TRUE) as active_clusters,
(SELECT COUNT(*) FROM haproxy_cluster_pools WHERE is_active = TRUE) as active_pools,
(SELECT COUNT(*) FROM agents) as total_agents,
(SELECT COUNT(*) FROM agents WHERE status = 'online') as online_agents,
(SELECT COUNT(*) FROM frontends WHERE is_active = TRUE) as active_frontends,
(SELECT COUNT(*) FROM backends WHERE is_active = TRUE) as active_backends
""")
await close_database_connection(conn)
return {
"clusters": {
"active": stats["active_clusters"] or 0,
"total": stats["active_clusters"] or 0
},
"pools": {
"active": stats["active_pools"] or 0,
"total": stats["active_pools"] or 0
},
"agents": {
"online": stats["online_agents"] or 0,
"total": stats["total_agents"] or 0,
"offline": (stats["total_agents"] or 0) - (stats["online_agents"] or 0)
},
"configurations": {
"frontends": stats["active_frontends"] or 0,
"backends": stats["active_backends"] or 0
}
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/pools", dependencies=[Depends(require_authenticated_user)]) # SECURITY (GHSA-3p5c): pool names/env/counts
async def get_pools():
"""Get all HAProxy cluster pools"""
try:
conn = await get_database_connection()
# Schema-safe pools query
try:
pools = await conn.fetch("""
SELECT p.id, p.name, p.description, p.environment, p.location,
p.is_active, p.created_at, p.updated_at,
COUNT(c.id) as cluster_count,
COUNT(a.id) as agent_count,
COUNT(CASE WHEN a.status = 'online' THEN 1 END) as online_agents,
COUNT(CASE WHEN a.status = 'offline' THEN 1 END) as offline_agents,
COUNT(CASE WHEN a.status = 'warning' THEN 1 END) as warning_agents
FROM haproxy_cluster_pools p
LEFT JOIN haproxy_clusters c ON c.pool_id = p.id AND c.is_active = TRUE
LEFT JOIN agents a ON a.pool_id = p.id
WHERE p.is_active = TRUE
GROUP BY p.id, p.name, p.description, p.environment, p.location,
p.is_active, p.created_at, p.updated_at
ORDER BY p.name
""")
except Exception as schema_error:
logger.warning(f"Schema error in pools query, using fallback: {schema_error}")
# Fallback query without location column
pools = await conn.fetch("""
SELECT p.id, p.name, p.description, p.environment,
p.is_active, p.created_at, p.updated_at,
COUNT(c.id) as cluster_count,
COUNT(a.id) as agent_count,
COUNT(CASE WHEN a.status = 'online' THEN 1 END) as online_agents,
COUNT(CASE WHEN a.status = 'offline' THEN 1 END) as offline_agents,
COUNT(CASE WHEN a.status = 'warning' THEN 1 END) as warning_agents
FROM haproxy_cluster_pools p
LEFT JOIN haproxy_clusters c ON c.pool_id = p.id AND c.is_active = TRUE
LEFT JOIN agents a ON a.pool_id = p.id
WHERE p.is_active = TRUE
GROUP BY p.id, p.name, p.description, p.environment,
p.is_active, p.created_at, p.updated_at
ORDER BY p.name
""")
await close_database_connection(conn)
return {
"pools": [
{
"id": pool["id"],
"name": pool["name"],
"description": pool.get("description"),
"environment": pool.get("environment"),
"location": pool.get("location", "unspecified"),
"is_active": pool.get("is_active", True),
"created_at": pool["created_at"].isoformat() if pool.get("created_at") else None,
"updated_at": pool["updated_at"].isoformat() if pool.get("updated_at") else None,
"cluster_count": pool.get("cluster_count", 0) or 0,
"agent_count": pool.get("agent_count", 0) or 0,
"online_agents": pool.get("online_agents", 0) or 0,
"offline_agents": pool.get("offline_agents", 0) or 0,
"warning_agents": pool.get("warning_agents", 0) or 0
}
for pool in pools
]
}
except Exception as e:
logger.error(f"Error fetching pools: {e}")
return {"pools": []}
@router.get("/haproxy-cluster-pools", dependencies=[Depends(require_authenticated_user)]) # SECURITY (GHSA-3p5c)
async def get_haproxy_cluster_pools():
"""Get all HAProxy cluster pools (legacy endpoint)"""
# Just call the main pools endpoint
return await get_pools()
@router.post("/pools")
async def create_pool(pool: PoolCreate, authorization: str = Header(None)):
"""Create a new HAProxy cluster pool"""
try:
# Get current user for activity logging
from auth_middleware import get_current_user_from_token
current_user = await get_current_user_from_token(authorization)
conn = await get_database_connection()
# Check if pool name already exists
existing = await conn.fetchrow("SELECT id FROM haproxy_cluster_pools WHERE name = $1", pool.name)
if existing:
await close_database_connection(conn)
raise HTTPException(status_code=400, detail=f"Pool '{pool.name}' already exists")
# Insert new pool
pool_id = await conn.fetchval("""
INSERT INTO haproxy_cluster_pools
(name, description, environment, location, is_active)
VALUES ($1, $2, $3, $4, $5)
RETURNING id
""", pool.name, pool.description, pool.environment, pool.location, pool.is_active)
# Automatically grant the creating user admin access to the new pool
try:
# Check if user_pool_access table exists
table_exists = await conn.fetchval("""
SELECT EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_name = 'user_pool_access'
)
""")
if table_exists:
await conn.execute("""
INSERT INTO user_pool_access (user_id, pool_id, access_level, granted_by)
VALUES ($1, $2, 'admin', $1)
ON CONFLICT (user_id, pool_id) DO NOTHING
""", current_user['id'], pool_id)
logger.info(f"Granted admin access to user {current_user['username']} for new pool '{pool.name}'")
else:
logger.warning("user_pool_access table not found, skipping automatic access grant")
except Exception as e:
logger.error(f"Failed to grant pool access to user: {e}")
# Don't fail the pool creation, just log the error
await close_database_connection(conn)
# Log user activity
if current_user and current_user.get('id'):
await log_user_activity(
user_id=current_user['id'],
action='create',
resource_type='pool',
resource_id=str(pool_id),
details={
'pool_name': pool.name,
'environment': pool.environment,
'location': pool.location
}
)
return {
"message": f"Pool '{pool.name}' created successfully",
"id": pool_id,
"pool": pool.dict()
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.put("/haproxy-cluster-pools/{pool_id}")
async def update_pool(pool_id: int, pool: PoolUpdate, authorization: str = Header(None)):
"""Update an existing HAProxy cluster pool"""
try:
from auth_middleware import get_current_user_from_token
current_user = await get_current_user_from_token(authorization)
conn = await get_database_connection()
# Check if pool exists
existing_pool = await conn.fetchrow("SELECT name FROM haproxy_cluster_pools WHERE id = $1", pool_id)
if not existing_pool:
await close_database_connection(conn)
raise HTTPException(status_code=404, detail="Pool not found")
# Update pool
await conn.execute("""
UPDATE haproxy_cluster_pools
SET name = $2, description = $3, location = $4, is_active = $5, environment = $6
WHERE id = $1
""", pool_id, pool.name, pool.description, pool.location, pool.is_active, pool.environment)
await close_database_connection(conn)
# Log activity
await log_user_activity(
user_id=current_user["id"],
action='update',
resource_type='pool',
resource_id=str(pool_id),
details={
'pool_name': pool.name,
'old_name': existing_pool['name']
}
)
return {"message": f"Pool '{pool.name}' updated successfully"}
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to update pool: {e}")
raise HTTPException(status_code=500, detail=f"Failed to update pool: {str(e)}")
@router.get("/haproxy-cluster-pools/{pool_id}/agents", dependencies=[Depends(require_authenticated_user)]) # SECURITY (GHSA-3p5c): full agent inventory — same class as GET /api/agents
async def get_pool_agents(pool_id: int):
"""Get all agents for a specific pool"""
try:
conn = await get_database_connection()
# First verify pool exists
pool = await conn.fetchrow("SELECT id, name FROM haproxy_cluster_pools WHERE id = $1", pool_id)
if not pool:
await close_database_connection(conn)
raise HTTPException(status_code=404, detail="Pool not found")
# Get agents for this pool
agents = await conn.fetch("""
SELECT a.id, a.name, a.pool_id, a.platform, a.architecture, a.version,
a.hostname, a.ip_address, a.operating_system, a.kernel_version,
a.uptime, a.cpu_count, a.memory_total, a.disk_space,
a.network_interfaces, a.capabilities, a.status, a.last_seen,
a.haproxy_status, a.haproxy_version, a.config_version,
COALESCE(a.enabled, TRUE) as enabled,
a.created_at, a.updated_at,
p.name as pool_name, p.environment as pool_environment
FROM agents a
LEFT JOIN haproxy_cluster_pools p ON a.pool_id = p.id
WHERE a.pool_id = $1
ORDER BY a.name
""", pool_id)
await close_database_connection(conn)
# Calculate health status for each agent
agent_list = []
for agent in agents:
# Calculate health based on status and last_seen
health = "offline"
if agent["status"] == "online" and agent["last_seen"]:
time_diff = datetime.utcnow() - agent["last_seen"]
if time_diff.total_seconds() < 60: # Less than 1 minute
health = "healthy"
elif time_diff.total_seconds() < 300: # Less than 5 minutes
health = "warning"
agent_list.append({
"id": agent["id"],
"name": agent["name"],
"pool_id": agent["pool_id"],
"platform": agent["platform"],
"architecture": agent["architecture"],
"version": agent["version"],
"hostname": agent["hostname"],
"ip_address": agent["ip_address"],
"operating_system": agent["operating_system"],
"kernel_version": agent["kernel_version"],
"uptime": agent["uptime"],
"cpu_count": agent["cpu_count"],
"memory_total": agent["memory_total"],
"disk_space": agent["disk_space"],
"network_interfaces": agent["network_interfaces"],
"capabilities": agent["capabilities"],
"status": agent["status"],
"health": health,
"last_seen": agent["last_seen"].isoformat() if agent["last_seen"] else None,
"haproxy_status": agent["haproxy_status"],
"haproxy_version": agent["haproxy_version"],
"config_version": agent["config_version"],
"enabled": agent["enabled"],
"created_at": agent["created_at"].isoformat() if agent["created_at"] else None,
"updated_at": agent["updated_at"].isoformat() if agent["updated_at"] else None,
"pool_name": agent["pool_name"],
"pool_environment": agent["pool_environment"]
})
return {
"pool": {
"id": pool["id"],
"name": pool["name"]
},
"agents": agent_list,
"total_agents": len(agent_list)
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error fetching pool agents: {e}")
raise HTTPException(status_code=500, detail=f"Failed to fetch pool agents: {str(e)}")
@router.get("/haproxy/stats", dependencies=[Depends(require_authenticated_user)]) # SECURITY (GHSA-3p5c)
async def get_haproxy_stats(cluster_id: Optional[int] = None):
"""Get HAProxy statistics"""
try:
conn = await get_database_connection()
# Get cluster info if cluster_id provided
cluster_info = None
if cluster_id:
cluster_info = await conn.fetchrow("""
SELECT id, name, connection_type, is_active
FROM haproxy_clusters
WHERE id = $1
""", cluster_id)
if not cluster_info:
await close_database_connection(conn)
raise HTTPException(status_code=404, detail="Cluster not found")
await close_database_connection(conn)
# Generate realistic mock stats for now
# In production, this would connect to HAProxy stats socket
import random
import time
base_requests = random.randint(50000, 200000)
current_time = int(time.time())
# Frontend stats (mocked)
frontend_stats = {
"requests_total": base_requests + random.randint(0, 1000),
"requests_rate": random.randint(50, 200),
"status": random.choice(["UP", "UNKNOWN"]),
"current_sessions": random.randint(10, 100),
"max_sessions": random.randint(200, 500),
"bytes_in": base_requests * random.randint(1000, 5000),
"bytes_out": base_requests * random.randint(2000, 8000),
"response_time_avg": random.randint(50, 200),
"error_rate": round(random.uniform(0.1, 2.0), 2)
}
# Backend stats (mocked)
backend_stats = []
backend_names = ["web_servers", "api_servers", "database_servers", "cache_servers"]
for backend_name in backend_names[:random.randint(2, 4)]:
backend_stats.append({
"name": backend_name,
"status": random.choice(["UP", "DOWN", "MAINT"]),
"current_sessions": random.randint(0, 50),
"max_sessions": random.randint(100, 300),
"queue_current": random.randint(0, 10),
"queue_max": random.randint(0, 50),
"servers_active": random.randint(1, 4),
"servers_total": random.randint(2, 6),
"bytes_in": random.randint(10000, 100000),
"bytes_out": random.randint(20000, 200000),
"response_time": random.randint(30, 150),
"error_rate": round(random.uniform(0.0, 1.5), 2)
})
# Server stats (mocked)
server_stats = []
for backend in backend_stats:
for i in range(backend["servers_total"]):
server_name = f"{backend['name']}_server_{i+1}"
server_stats.append({
"backend": backend["name"],
"name": server_name,
"status": random.choice(["UP", "DOWN", "MAINT"]) if i < backend["servers_active"] else "DOWN",
"weight": random.randint(50, 100),
"current_sessions": random.randint(0, 20),
"max_sessions": random.randint(50, 150),
"check_status": random.choice(["L7OK", "L4OK", "L6OK", "DOWN"]),
"last_check": f"{random.randint(0, 1000)}ms",
"downtime": random.randint(0, 3600) if random.choice([True, False]) else 0
})
# Traffic distribution (mocked)
traffic_distribution = []
for i in range(24): # 24 hours of data
hour = (current_time - (23-i) * 3600)
traffic_distribution.append({
"timestamp": hour,
"requests": random.randint(1000, 5000),
"bandwidth": random.randint(50000, 200000),
"errors": random.randint(5, 50)
})
# Overall cluster health
overall_health = {
"status": "healthy" if random.choice([True, True, True, False]) else "warning",
"total_frontends": len(frontend_stats),
"total_backends": len(backend_stats),
"total_servers": len(server_stats),
"servers_up": len([s for s in server_stats if s["status"] == "UP"]),
"cpu_usage": round(random.uniform(20, 80), 1),
"memory_usage": round(random.uniform(30, 70), 1),
"uptime_seconds": random.randint(100000, 1000000)
}
return {
"cluster_id": cluster_id,
"cluster_name": cluster_info["name"] if cluster_info else "Global",
"timestamp": current_time,
"frontend_stats": frontend_stats,
"backend_stats": backend_stats,
"server_stats": server_stats,
"traffic_distribution": traffic_distribution,
"overall_health": overall_health,
"data_source": "mock" # In production this would be "haproxy_socket"
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to get HAProxy stats: {e}")
raise HTTPException(status_code=500, detail=f"Failed to get HAProxy stats: {str(e)}")
@router.delete("/haproxy-cluster-pools/{pool_id}") # Fixed route path
async def delete_pool(pool_id: int, authorization: str = Header(None)):
"""Delete a HAProxy cluster pool"""
try:
# Get current user for activity logging
from auth_middleware import get_current_user_from_token
current_user = await get_current_user_from_token(authorization)
conn = await get_database_connection()
# Get pool info before deletion
pool = await conn.fetchrow("SELECT name FROM haproxy_cluster_pools WHERE id = $1", pool_id)
if not pool:
await close_database_connection(conn)
raise HTTPException(status_code=404, detail="Pool not found")
# Check for dependencies before deletion
dependencies = []
# Check clusters
clusters_count = await conn.fetchval("SELECT COUNT(*) FROM haproxy_clusters WHERE pool_id = $1", pool_id)
if clusters_count > 0:
dependencies.append(f"{clusters_count} cluster(s)")
# Check agents
agents_count = await conn.fetchval("SELECT COUNT(*) FROM agents WHERE pool_id = $1", pool_id)
if agents_count > 0:
dependencies.append(f"{agents_count} agent(s)")
# If dependencies exist, prevent deletion
if dependencies:
await close_database_connection(conn)
deps_text = ", ".join(dependencies)
raise HTTPException(
status_code=400,
detail=f"Cannot delete pool: it has {deps_text}. Please remove these dependencies first."
)
# Delete the pool
await conn.execute("DELETE FROM haproxy_cluster_pools WHERE id = $1", pool_id)
await close_database_connection(conn)
# Log user activity
if current_user and current_user.get('id'):
await log_user_activity(
user_id=current_user['id'],
action='delete',
resource_type='pool',
resource_id=str(pool_id),
details={'pool_name': pool['name']}
)
return {"message": f"Pool '{pool['name']}' deleted successfully"}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/pools/grant-current-user-access")
async def grant_current_user_pool_access(authorization: str = Header(None)):
"""Grant current user access to all pools - emergency access for testing"""
try:
# Get current user
from auth_middleware import get_current_user_from_token
current_user = await get_current_user_from_token(authorization)
conn = await get_database_connection()
# Check if user_pool_access table exists
table_exists = await conn.fetchval("""
SELECT EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_name = 'user_pool_access'
)
""")
if not table_exists:
await close_database_connection(conn)
raise HTTPException(status_code=500, detail="user_pool_access table not found")
# Grant access to all pools
await conn.execute("""
INSERT INTO user_pool_access (user_id, pool_id, access_level, granted_by)
SELECT $1, p.id, 'admin', $1
FROM haproxy_cluster_pools p
ON CONFLICT (user_id, pool_id)
DO UPDATE SET
access_level = 'admin',
granted_by = EXCLUDED.granted_by,
granted_at = CURRENT_TIMESTAMP,
is_active = TRUE
""", current_user['id'])
await close_database_connection(conn)
return {
"message": f"Access granted to all pools for user {current_user['username']}",
"user_id": current_user['id']
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to grant current user pool access: {e}")
raise HTTPException(status_code=500, detail=f"Failed to grant current user pool access: {str(e)}")