core-ai - OBSOLETE
This commit is contained in:
@@ -287,6 +287,180 @@ class InfrastructureController(BaseController):
|
||||
logger.error(f"Failed to get service '{name}': {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.post(
|
||||
"/services/{service}/manage",
|
||||
response_model=Dict[str, Any],
|
||||
summary="Manage service lifecycle"
|
||||
)
|
||||
async def manage_service(service: str, action: str, replicas: Optional[int] = None):
|
||||
"""
|
||||
Manage Docker Compose service/stack lifecycle.
|
||||
|
||||
Args:
|
||||
service: Service/stack name
|
||||
action: One of: "start", "stop", "restart", "scale"
|
||||
replicas: Number of replicas (required for scale action)
|
||||
|
||||
Returns:
|
||||
Success status and message
|
||||
"""
|
||||
from pydantic import BaseModel
|
||||
|
||||
class ManageServiceRequest(BaseModel):
|
||||
action: str
|
||||
replicas: Optional[int] = None
|
||||
|
||||
portainer = get_portainer_client()
|
||||
logger.info(f"Managing service '{service}': action={action}, replicas={replicas}")
|
||||
|
||||
# Validate action
|
||||
valid_actions = ["start", "stop", "restart", "scale"]
|
||||
if action not in valid_actions:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Invalid action '{action}'. Must be one of: {', '.join(valid_actions)}"
|
||||
)
|
||||
|
||||
# Validate scale has replicas
|
||||
if action == "scale" and replicas is None:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="'scale' action requires 'replicas' parameter"
|
||||
)
|
||||
|
||||
try:
|
||||
# Find the stack by name
|
||||
stacks = await portainer.get_stacks()
|
||||
stack = next(
|
||||
(s for s in stacks if s.get("Name", "").lower() == service.lower()),
|
||||
None
|
||||
)
|
||||
|
||||
if not stack:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Service '{service}' not found"
|
||||
)
|
||||
|
||||
stack_id = stack.get("Id")
|
||||
endpoint_id = stack.get("EndpointId")
|
||||
|
||||
# Perform the action
|
||||
if action in ["start", "stop", "restart"]:
|
||||
# These actions need to be implemented
|
||||
# For now, return not implemented
|
||||
raise HTTPException(
|
||||
status_code=501,
|
||||
detail=f"Action '{action}' not yet implemented for services"
|
||||
)
|
||||
|
||||
elif action == "scale":
|
||||
# Scaling requires updating the stack's compose file
|
||||
raise HTTPException(
|
||||
status_code=501,
|
||||
detail="Scaling not yet implemented"
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"action": action,
|
||||
"service": service,
|
||||
"message": f"Action '{action}' completed successfully"
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to {action} service '{service}': {e}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.get(
|
||||
"/services/{service}/status",
|
||||
response_model=Dict[str, Any],
|
||||
summary="Get detailed service status"
|
||||
)
|
||||
async def get_service_status(service: str):
|
||||
"""
|
||||
Get comprehensive service status including containers, resources, and events.
|
||||
|
||||
Args:
|
||||
service: Service/stack name
|
||||
|
||||
Returns:
|
||||
Detailed status information
|
||||
"""
|
||||
portainer = get_portainer_client()
|
||||
logger.info(f"Getting status for service '{service}'")
|
||||
|
||||
try:
|
||||
# Find the stack
|
||||
stacks = await portainer.get_stacks()
|
||||
stack = next(
|
||||
(s for s in stacks if s.get("Name", "").lower() == service.lower()),
|
||||
None
|
||||
)
|
||||
|
||||
if not stack:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Service '{service}' not found"
|
||||
)
|
||||
|
||||
stack_id = stack.get("Id")
|
||||
endpoint_id = stack.get("EndpointId")
|
||||
status_code = stack.get("Status", 0)
|
||||
|
||||
# Get containers for this stack
|
||||
all_containers = await portainer.list_containers(all_containers=True)
|
||||
|
||||
# Filter containers belonging to this stack
|
||||
# Stack name is usually in the container labels
|
||||
stack_containers = []
|
||||
for container in all_containers:
|
||||
labels = container.get('Labels', {})
|
||||
# Check if container belongs to this stack
|
||||
# Docker Compose adds labels like: com.docker.compose.project
|
||||
project = labels.get('com.docker.compose.project', '').lower()
|
||||
if project == service.lower():
|
||||
stack_containers.append(container)
|
||||
|
||||
# Format container info
|
||||
container_info = []
|
||||
running_count = 0
|
||||
for c in stack_containers:
|
||||
state = c.get('State', 'unknown')
|
||||
if state == 'running':
|
||||
running_count += 1
|
||||
|
||||
container_info.append({
|
||||
'name': c.get('Names', ['unknown'])[0].lstrip('/'),
|
||||
'status': state,
|
||||
'health': 'N/A', # Would need to inspect container for health
|
||||
'uptime': c.get('Status', 'N/A')
|
||||
})
|
||||
|
||||
total_containers = len(stack_containers)
|
||||
replica_status = f"{running_count}/{total_containers} running"
|
||||
|
||||
return {
|
||||
"name": stack.get("Name"),
|
||||
"status": "active" if status_code == 1 else "inactive",
|
||||
"stack_id": stack_id,
|
||||
"replica_status": replica_status,
|
||||
"containers": container_info,
|
||||
"resources": {
|
||||
"memory_total": "N/A", # Would need to aggregate container stats
|
||||
"cpu_usage": "N/A"
|
||||
},
|
||||
"recent_events": [] # Would need to query Docker events API
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get status for service '{service}': {e}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.get(
|
||||
"/ports",
|
||||
response_model=List[PortInfo],
|
||||
@@ -1284,6 +1458,474 @@ class InfrastructureController(BaseController):
|
||||
logger.error(f"Failed to delete monitor {monitor_id}: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to delete monitor: {str(e)}")
|
||||
|
||||
# ========================================================================
|
||||
# Container Management Endpoints (for core-ai infrastructure tools)
|
||||
# ========================================================================
|
||||
|
||||
@router.get(
|
||||
"/containers",
|
||||
response_model=List[Dict[str, Any]],
|
||||
summary="List Docker containers"
|
||||
)
|
||||
async def list_containers(status: Optional[str] = "running"):
|
||||
"""
|
||||
List Docker containers with optional status filter.
|
||||
|
||||
Args:
|
||||
status: Filter by status - "all", "running", "stopped", "paused" (default: "running")
|
||||
|
||||
Returns:
|
||||
List of containers in Docker API format
|
||||
"""
|
||||
portainer = get_portainer_client()
|
||||
logger.info(f"Listing containers (status filter: {status})")
|
||||
|
||||
try:
|
||||
# Get all containers using Portainer with Docker socket fallback
|
||||
all_containers_flag = status in ["all", "stopped"]
|
||||
containers = await portainer.list_containers(all_containers=all_containers_flag)
|
||||
|
||||
# Filter by status if needed
|
||||
if status == "running":
|
||||
containers = [c for c in containers if c.get('State') == 'running']
|
||||
elif status == "stopped":
|
||||
containers = [c for c in containers if c.get('State') != 'running']
|
||||
elif status == "paused":
|
||||
containers = [c for c in containers if c.get('State') == 'paused']
|
||||
|
||||
logger.info(f"Found {len(containers)} containers matching status '{status}'")
|
||||
return containers
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to list containers: {e}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=f"Failed to list containers: {str(e)}")
|
||||
|
||||
@router.post(
|
||||
"/containers/{container}/{action}",
|
||||
response_model=Dict[str, Any],
|
||||
summary="Manage container state"
|
||||
)
|
||||
async def manage_container(container: str, action: str):
|
||||
"""
|
||||
Perform lifecycle operation on a Docker container.
|
||||
|
||||
Args:
|
||||
container: Container name or ID
|
||||
action: One of: "start", "stop", "restart", "pause", "unpause", "remove"
|
||||
|
||||
Returns:
|
||||
Success status and message
|
||||
"""
|
||||
portainer = get_portainer_client()
|
||||
logger.info(f"Managing container '{container}': action={action}")
|
||||
|
||||
# Validate action
|
||||
valid_actions = ["start", "stop", "restart", "pause", "unpause", "remove"]
|
||||
if action not in valid_actions:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Invalid action '{action}'. Must be one of: {', '.join(valid_actions)}"
|
||||
)
|
||||
|
||||
try:
|
||||
# Find the container
|
||||
all_containers = await portainer.list_containers(all_containers=True)
|
||||
|
||||
matching_container = None
|
||||
for c in all_containers:
|
||||
names = c.get('Names', [])
|
||||
for name in names:
|
||||
clean_name = name.lstrip('/')
|
||||
if clean_name.lower() == container.lower():
|
||||
matching_container = c
|
||||
break
|
||||
if matching_container:
|
||||
break
|
||||
|
||||
if not matching_container:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Container '{container}' not found"
|
||||
)
|
||||
|
||||
container_id = matching_container['Id']
|
||||
|
||||
# Get endpoints
|
||||
endpoints = await portainer.get_endpoints()
|
||||
if not endpoints:
|
||||
raise HTTPException(status_code=500, detail="No Portainer endpoints available")
|
||||
|
||||
endpoint_id = endpoints[0]['Id']
|
||||
|
||||
# Perform the action
|
||||
if action == "start":
|
||||
await portainer.start_container(endpoint_id, container_id)
|
||||
message = f"Started container '{container}' successfully"
|
||||
|
||||
elif action == "stop":
|
||||
await portainer.stop_container(endpoint_id, container_id)
|
||||
message = f"Stopped container '{container}' successfully"
|
||||
|
||||
elif action == "restart":
|
||||
await portainer.stop_container(endpoint_id, container_id)
|
||||
await portainer.start_container(endpoint_id, container_id)
|
||||
message = f"Restarted container '{container}' successfully"
|
||||
|
||||
elif action in ["pause", "unpause", "remove"]:
|
||||
# These actions need to be added to Portainer client
|
||||
# For now, return error
|
||||
raise HTTPException(
|
||||
status_code=501,
|
||||
detail=f"Action '{action}' not yet implemented"
|
||||
)
|
||||
|
||||
logger.info(f"Successfully {action}ed container '{container}'")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"action": action,
|
||||
"container": container,
|
||||
"message": message
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to {action} container '{container}': {e}", exc_info=True)
|
||||
|
||||
# Provide helpful error messages
|
||||
error_str = str(e).lower()
|
||||
if "304" in error_str or "not modified" in error_str:
|
||||
raise HTTPException(
|
||||
status_code=304,
|
||||
detail=f"Container '{container}' is already in the target state"
|
||||
)
|
||||
elif "conflict" in error_str:
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail=f"Cannot {action} container '{container}': state conflict"
|
||||
)
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.get(
|
||||
"/containers/{container}",
|
||||
response_model=Dict[str, Any],
|
||||
summary="Inspect container"
|
||||
)
|
||||
async def inspect_container(
|
||||
container: str,
|
||||
details: Optional[str] = "summary"
|
||||
):
|
||||
"""
|
||||
Get detailed information about a Docker container.
|
||||
|
||||
Args:
|
||||
container: Container name or ID
|
||||
details: Level of detail - "summary", "full", or "resources" (not used server-side, for client formatting)
|
||||
|
||||
Returns:
|
||||
Container details in Docker inspect format
|
||||
"""
|
||||
portainer = get_portainer_client()
|
||||
logger.info(f"Inspecting container '{container}' (details={details})")
|
||||
|
||||
try:
|
||||
# Use Portainer's inspect_container which auto-detects endpoint and falls back to Docker socket
|
||||
info = await portainer.inspect_container(container)
|
||||
|
||||
if not info:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Container '{container}' not found"
|
||||
)
|
||||
|
||||
logger.info(f"Successfully inspected container '{container}'")
|
||||
return info
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to inspect container '{container}': {e}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.get(
|
||||
"/containers/{container}/logs",
|
||||
response_model=Dict[str, Any],
|
||||
summary="Get container logs"
|
||||
)
|
||||
async def get_container_logs(
|
||||
container: str,
|
||||
lines: Optional[int] = 50,
|
||||
since: Optional[str] = None
|
||||
):
|
||||
"""
|
||||
Retrieve logs from a Docker container.
|
||||
|
||||
Args:
|
||||
container: Container name or ID
|
||||
lines: Number of log lines to retrieve (default: 50, max: 500)
|
||||
since: Time filter - "1h", "30m", or ISO timestamp (not yet implemented)
|
||||
|
||||
Returns:
|
||||
Container logs with metadata
|
||||
"""
|
||||
import httpx
|
||||
|
||||
logger.info(f"Retrieving logs for container '{container}' (lines={lines}, since={since})")
|
||||
|
||||
# Clamp lines to reasonable limit
|
||||
lines = min(max(1, lines), 500)
|
||||
|
||||
try:
|
||||
# Access Docker socket directly to get logs
|
||||
transport = httpx.AsyncHTTPTransport(uds="/var/run/docker.sock")
|
||||
async with httpx.AsyncClient(transport=transport, timeout=15.0) as client:
|
||||
# Get container logs via Docker API
|
||||
params = {
|
||||
"stdout": "true",
|
||||
"stderr": "true",
|
||||
"tail": lines,
|
||||
"timestamps": "true"
|
||||
}
|
||||
|
||||
# TODO: Add 'since' parameter support
|
||||
# if since:
|
||||
# params["since"] = parse_since_to_timestamp(since)
|
||||
|
||||
response = await client.get(
|
||||
f"http://localhost/v1.41/containers/{container}/logs",
|
||||
params=params
|
||||
)
|
||||
|
||||
if response.status_code == 404:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Container '{container}' not found"
|
||||
)
|
||||
|
||||
response.raise_for_status()
|
||||
logs_raw = response.text
|
||||
|
||||
# Docker logs come with binary prefixes (8 bytes per line)
|
||||
# Strip these headers for cleaner output
|
||||
lines_list = logs_raw.split('\n')
|
||||
cleaned_lines = []
|
||||
|
||||
for line in lines_list:
|
||||
if len(line) > 8:
|
||||
# Skip the 8-byte Docker stream header if present
|
||||
cleaned_line = line[8:] if line[0:1] in [b'\x01', b'\x02', '\x01', '\x02'] else line
|
||||
cleaned_lines.append(cleaned_line)
|
||||
elif line:
|
||||
cleaned_lines.append(line)
|
||||
|
||||
logs = '\n'.join(cleaned_lines).strip()
|
||||
|
||||
logger.info(f"Successfully retrieved logs for container '{container}' ({len(cleaned_lines)} lines)")
|
||||
|
||||
return {
|
||||
"container": container,
|
||||
"lines_requested": lines,
|
||||
"since": since,
|
||||
"logs": logs
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to retrieve logs for container '{container}': {e}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
# ========================================================================
|
||||
# Monitoring & Resource Endpoints (for core-ai infrastructure tools)
|
||||
# ========================================================================
|
||||
|
||||
@router.get(
|
||||
"/resources/system",
|
||||
response_model=Dict[str, Any],
|
||||
summary="Get system resource usage"
|
||||
)
|
||||
async def get_system_resources():
|
||||
"""
|
||||
Get overall system resource usage.
|
||||
|
||||
Returns:
|
||||
System-level metrics including CPU, memory, disk, and network
|
||||
"""
|
||||
logger.info("Getting system resources")
|
||||
|
||||
try:
|
||||
# Access Docker system info via socket
|
||||
import httpx
|
||||
|
||||
transport = httpx.AsyncHTTPTransport(uds="/var/run/docker.sock")
|
||||
async with httpx.AsyncClient(transport=transport, timeout=10.0) as client:
|
||||
# Get system info
|
||||
info_response = await client.get("http://localhost/v1.41/info")
|
||||
info_response.raise_for_status()
|
||||
info = info_response.json()
|
||||
|
||||
# Get system df (disk usage)
|
||||
df_response = await client.get("http://localhost/v1.41/system/df")
|
||||
df_response.raise_for_status()
|
||||
df = df_response.json()
|
||||
|
||||
# Extract system metrics
|
||||
ncpu = info.get('NCPU', 0)
|
||||
mem_total = info.get('MemTotal', 0)
|
||||
|
||||
# Calculate memory usage (rough estimate)
|
||||
# Docker doesn't directly provide used memory, so we estimate
|
||||
mem_used = mem_total * 0.5 # Placeholder - would need psutil or /proc
|
||||
mem_available = mem_total - mem_used
|
||||
mem_usage_pct = (mem_used / mem_total * 100) if mem_total > 0 else 0
|
||||
|
||||
# Disk usage from system/df
|
||||
layers_size = sum(img.get('Size', 0) for img in df.get('Images', []))
|
||||
containers_size = sum(c.get('SizeRw', 0) for c in df.get('Containers', []))
|
||||
volumes_size = sum(v.get('UsageData', {}).get('Size', 0) for v in df.get('Volumes', []))
|
||||
|
||||
total_disk_used = layers_size + containers_size + volumes_size
|
||||
|
||||
return {
|
||||
"cpu": {
|
||||
"cores": ncpu,
|
||||
"usage_percent": None, # Would need to calculate from stats over time
|
||||
"load_average": [] # Not available from Docker API
|
||||
},
|
||||
"memory": {
|
||||
"total_bytes": mem_total,
|
||||
"used_bytes": mem_used,
|
||||
"available_bytes": mem_available,
|
||||
"usage_percent": mem_usage_pct
|
||||
},
|
||||
"disk": {
|
||||
"total_bytes": None, # Not available from Docker API
|
||||
"used_bytes": total_disk_used,
|
||||
"available_bytes": None,
|
||||
"usage_percent": None
|
||||
},
|
||||
"network": {
|
||||
"interfaces": {} # Would need to parse network stats
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get system resources: {e}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.get(
|
||||
"/resources/containers",
|
||||
response_model=List[Dict[str, Any]],
|
||||
summary="Get container resource usage"
|
||||
)
|
||||
async def get_container_resources(container: Optional[str] = None):
|
||||
"""
|
||||
Get container-specific resource usage.
|
||||
|
||||
Args:
|
||||
container: Optional specific container name/ID
|
||||
|
||||
Returns:
|
||||
List of container resource stats
|
||||
"""
|
||||
logger.info(f"Getting container resources (container={container})")
|
||||
|
||||
try:
|
||||
import httpx
|
||||
portainer = get_portainer_client()
|
||||
|
||||
# Get list of containers
|
||||
if container:
|
||||
# Get specific container
|
||||
all_containers = await portainer.list_containers(all_containers=False)
|
||||
containers = [c for c in all_containers
|
||||
if container.lower() in c.get('Names', [''])[0].lower()]
|
||||
|
||||
if not containers:
|
||||
raise HTTPException(status_code=404, detail=f"Container '{container}' not found")
|
||||
else:
|
||||
# Get all running containers
|
||||
containers = await portainer.list_containers(all_containers=False)
|
||||
|
||||
# Get stats for each container
|
||||
stats_list = []
|
||||
transport = httpx.AsyncHTTPTransport(uds="/var/run/docker.sock")
|
||||
|
||||
async with httpx.AsyncClient(transport=transport, timeout=15.0) as client:
|
||||
for c in containers:
|
||||
container_id = c.get('Id')
|
||||
name = c.get('Names', ['unknown'])[0].lstrip('/')
|
||||
|
||||
try:
|
||||
# Get container stats (one-time, not streaming)
|
||||
stats_response = await client.get(
|
||||
f"http://localhost/v1.41/containers/{container_id}/stats",
|
||||
params={"stream": "false"}
|
||||
)
|
||||
stats_response.raise_for_status()
|
||||
stats = stats_response.json()
|
||||
|
||||
# Parse stats
|
||||
cpu_stats = stats.get('cpu_stats', {})
|
||||
precpu_stats = stats.get('precpu_stats', {})
|
||||
memory_stats = stats.get('memory_stats', {})
|
||||
networks = stats.get('networks', {})
|
||||
blkio_stats = stats.get('blkio_stats', {})
|
||||
|
||||
# Calculate CPU percentage
|
||||
cpu_delta = cpu_stats.get('cpu_usage', {}).get('total_usage', 0) - \
|
||||
precpu_stats.get('cpu_usage', {}).get('total_usage', 0)
|
||||
system_delta = cpu_stats.get('system_cpu_usage', 0) - \
|
||||
precpu_stats.get('system_cpu_usage', 0)
|
||||
online_cpus = cpu_stats.get('online_cpus', 1)
|
||||
|
||||
cpu_percent = 0.0
|
||||
if system_delta > 0 and cpu_delta > 0:
|
||||
cpu_percent = (cpu_delta / system_delta) * online_cpus * 100.0
|
||||
|
||||
# Memory stats
|
||||
mem_usage = memory_stats.get('usage', 0)
|
||||
mem_limit = memory_stats.get('limit', 0)
|
||||
mem_percent = (mem_usage / mem_limit * 100) if mem_limit > 0 else 0
|
||||
|
||||
# Network stats
|
||||
net_rx = sum(net.get('rx_bytes', 0) for net in networks.values())
|
||||
net_tx = sum(net.get('tx_bytes', 0) for net in networks.values())
|
||||
|
||||
# Block I/O stats
|
||||
io_service_bytes = blkio_stats.get('io_service_bytes_recursive', [])
|
||||
block_read = sum(entry.get('value', 0) for entry in io_service_bytes
|
||||
if entry.get('op') == 'Read')
|
||||
block_write = sum(entry.get('value', 0) for entry in io_service_bytes
|
||||
if entry.get('op') == 'Write')
|
||||
|
||||
stats_list.append({
|
||||
"name": name,
|
||||
"cpu_percent": cpu_percent,
|
||||
"memory_usage_bytes": mem_usage,
|
||||
"memory_limit_bytes": mem_limit,
|
||||
"memory_percent": mem_percent,
|
||||
"network_rx_bytes": net_rx,
|
||||
"network_tx_bytes": net_tx,
|
||||
"block_read_bytes": block_read,
|
||||
"block_write_bytes": block_write
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to get stats for container {name}: {e}")
|
||||
# Skip containers that fail to get stats
|
||||
continue
|
||||
|
||||
return stats_list
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get container resources: {e}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
return router
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user