core-ai - OBSOLETE

This commit is contained in:
2025-12-07 19:26:40 +01:00
parent 78c0fdf6ec
commit 31e353a4ba
56 changed files with 5191 additions and 2293 deletions
+446
View File
@@ -0,0 +1,446 @@
# Requested Core-API Services for Core-AI Infrastructure Tools
This document specifies the API endpoints needed by core-ai infrastructure tools. All requests from core-ai should go through core-api for centralized logging and access control.
## Context
The core-ai service is implementing 9 infrastructure tools in 3 logical clusters:
1. **Container Lifecycle** (4 tools) - containers.py
2. **Service Management** (3 tools) - services.py
3. **Monitoring & Resources** (2 tools) - monitoring.py
These tools need corresponding core-api REST endpoints to perform operations via Portainer.
---
## Cluster 1: Container Lifecycle Management
### 1.1 List Containers
**Endpoint:** `GET /v1/infrastructure/containers`
**Query Parameters:**
- `status` (optional): Filter by status - "all", "running", "stopped", "paused" (default: "running")
**Response:**
```json
[
{
"Id": "abc123...",
"Names": ["/nginx"],
"State": "running",
"Status": "Up 3 days",
"Image": "nginx:latest",
"Ports": [
{"PrivatePort": 80, "PublicPort": 8080, "Type": "tcp"},
{"PrivatePort": 443, "PublicPort": 8443, "Type": "tcp"}
],
"StartedAt": "2024-12-01T10:00:00Z"
}
]
```
**Implementation Notes:**
- Use `PortainerClient.list_containers(all_containers=True)` with Docker socket fallback
- Filter results based on `status` query parameter
- Return standard Docker API container list format
---
### 1.2 Manage Container
**Endpoint:** `POST /v1/infrastructure/containers/{container}/{action}`
**Path Parameters:**
- `container`: Container name or ID (e.g., "nginx", "core-ai")
- `action`: One of: "start", "stop", "restart", "pause", "unpause", "remove"
**Response (Success):**
```json
{
"success": true,
"action": "restart",
"container": "nginx",
"message": "Container restarted successfully"
}
```
**Response (Error):**
```json
{
"success": false,
"error": "Container not found",
"message": "Container 'nginx2' not found. Available containers: nginx, core-ai, ollama"
}
```
**Status Codes:**
- `200` - Success
- `304` - Not Modified (already in target state)
- `404` - Container not found
- `409` - Conflict (e.g., cannot remove running container)
- `500` - Server error
**Implementation Notes:**
- For "restart": call stop then start
- For actions not yet in PortainerClient (pause, unpause, remove):
- Call Portainer API directly: `/api/endpoints/{endpoint_id}/docker/containers/{container_id}/{action}`
- Handle partial name matching (case-insensitive)
- Return helpful error messages suggesting `docker_list_containers()` when not found
---
### 1.3 Inspect Container
**Endpoint:** `GET /v1/infrastructure/containers/{container}`
**Path Parameters:**
- `container`: Container name or ID
**Query Parameters:**
- `details` (optional): Level of detail - "summary" (default), "full", "resources"
**Response:**
```json
{
"Id": "abc123...",
"Name": "/nginx",
"State": {
"Status": "running",
"Running": true,
"StartedAt": "2024-12-01T10:00:00Z",
"FinishedAt": "0001-01-01T00:00:00Z",
"ExitCode": 0
},
"Config": {
"Image": "nginx:latest",
"Env": ["PATH=/usr/local/sbin:...", "NGINX_VERSION=1.25.0"],
"Cmd": ["nginx", "-g", "daemon off;"]
},
"NetworkSettings": {
"Ports": {
"80/tcp": [{"HostIp": "0.0.0.0", "HostPort": "8080"}],
"443/tcp": [{"HostIp": "0.0.0.0", "HostPort": "8443"}]
},
"Networks": {
"bridge": {
"IPAddress": "172.17.0.2",
"Gateway": "172.17.0.1"
}
}
},
"HostConfig": {
"Memory": 536870912,
"NanoCpus": 1000000000,
"RestartPolicy": {"Name": "unless-stopped"}
},
"Mounts": [
{
"Type": "bind",
"Source": "/host/path",
"Destination": "/container/path"
}
]
}
```
**Implementation Notes:**
- Use `PortainerClient.inspect_container(container)` which auto-detects endpoint and falls back to Docker socket
- Return full Docker inspect response
- The core-ai tool will handle formatting based on `details` level
- Return 404 if container not found
---
### 1.4 Container Logs
**Endpoint:** `GET /v1/infrastructure/containers/{container}/logs`
**Path Parameters:**
- `container`: Container name or ID
**Query Parameters:**
- `lines` (optional): Number of log lines (default: 50, max: 500)
- `since` (optional): Time filter - "1h", "30m", or ISO timestamp
**Response:**
```json
{
"container": "nginx",
"lines_requested": 50,
"since": null,
"logs": "2024-12-04T10:00:00.123Z Starting nginx...\n2024-12-04T10:00:01.456Z Ready to accept connections\n..."
}
```
**Implementation Notes:**
- Access Docker API directly: `GET /v1.41/containers/{container}/logs`
- Use Docker socket transport (httpx with uds)
- Parameters: `stdout=true`, `stderr=true`, `tail={lines}`, `timestamps=true`
- If `since` provided: add `since={unix_timestamp}` parameter
- Strip Docker stream headers (8-byte binary prefix per line)
- Return plain text logs with timestamps
- Return 404 if container not found
---
## Cluster 2: Service Management
### 2.1 List Services
**Endpoint:** `GET /v1/infrastructure/services` (already exists, may need enhancement)
**Query Parameters:**
- `stack` (optional): Filter by stack name
**Response:**
```json
[
{
"name": "portainer",
"stack_id": 1,
"status": "active",
"containers_running": 3,
"containers_total": 3,
"ports": [9000, 8000],
"domains": ["portainer.example.com"]
}
]
```
**Implementation Notes:**
- Enhance existing `/infrastructure/services` endpoint if needed
- Ensure it returns stack/service information from Portainer
- Include container counts (running/total)
---
### 2.2 Manage Service
**Endpoint:** `POST /v1/infrastructure/services/{service}/{action}`
**Path Parameters:**
- `service`: Service/stack name
- `action`: One of: "start", "stop", "restart", "scale"
**Request Body (for scale action):**
```json
{
"replicas": 3
}
```
**Response:**
```json
{
"success": true,
"action": "restart",
"service": "web",
"message": "Service restarted successfully"
}
```
**Implementation Notes:**
- For "start"/"stop": Use Portainer stack start/stop API
- For "restart": Stop then start the stack
- For "scale": Update stack with new replica count
- This may require updating stack compose file
---
### 2.3 Service Status
**Endpoint:** `GET /v1/infrastructure/services/{service}/status`
**Path Parameters:**
- `service`: Service/stack name
**Response:**
```json
{
"name": "web",
"status": "active",
"stack_id": 5,
"containers": [
{
"name": "web_app_1",
"status": "running",
"health": "healthy",
"uptime": "2 days"
}
],
"replica_status": "3/3 running",
"resources": {
"memory_total": "1.2 GB",
"cpu_usage": "15%"
},
"recent_events": [
{"time": "2024-12-04T09:00:00Z", "action": "container_start", "container": "web_app_3"}
]
}
```
**Implementation Notes:**
- Get stack details from Portainer
- Get individual container statuses
- Calculate aggregate resource usage
- May require querying Docker events API for recent events
---
## Cluster 3: Monitoring & Resources
### 3.1 System Resources
**Endpoint:** `GET /v1/infrastructure/resources/system`
**Response:**
```json
{
"cpu": {
"cores": 8,
"usage_percent": 45.2,
"load_average": [2.5, 2.3, 2.1]
},
"memory": {
"total_bytes": 16777216000,
"used_bytes": 8388608000,
"available_bytes": 8388608000,
"usage_percent": 50.0
},
"disk": {
"total_bytes": 500000000000,
"used_bytes": 250000000000,
"available_bytes": 250000000000,
"usage_percent": 50.0
},
"network": {
"interfaces": {
"eth0": {
"rx_bytes": 1000000000,
"tx_bytes": 500000000
}
}
}
}
```
**Implementation Notes:**
- Use Docker system info API: `GET /v1.41/system/df`
- May also use `GET /v1.41/info` for system-wide stats
- Calculate percentages and format nicely
- Include load averages from system stats
---
### 3.2 Container Resources
**Endpoint:** `GET /v1/infrastructure/resources/containers`
**Query Parameters:**
- `container` (optional): Specific container name/ID (if omitted, return all)
**Response:**
```json
[
{
"name": "nginx",
"cpu_percent": 5.2,
"memory_usage_bytes": 45000000,
"memory_limit_bytes": 100000000,
"memory_percent": 45.0,
"network_rx_bytes": 50000000,
"network_tx_bytes": 25000000,
"block_read_bytes": 10000000,
"block_write_bytes": 5000000
}
]
```
**Implementation Notes:**
- Use Docker stats API: `GET /v1.41/containers/{id}/stats?stream=false`
- If `container` param provided: return single container stats
- If omitted: return stats for all running containers
- Calculate percentages where applicable
- Stats API returns real-time metrics (one-time snapshot, not streaming)
---
## Implementation Priority
**Phase 1 (Needed immediately for core-ai):**
1. `GET /v1/infrastructure/containers` - List containers
2. `POST /v1/infrastructure/containers/{container}/{action}` - Manage containers
3. `GET /v1/infrastructure/containers/{container}` - Inspect container
4. `GET /v1/infrastructure/containers/{container}/logs` - Container logs
**Phase 2 (Needed for full infrastructure tools):**
5. `POST /v1/infrastructure/services/{service}/{action}` - Manage services
6. `GET /v1/infrastructure/services/{service}/status` - Service status
7. `GET /v1/infrastructure/resources/system` - System resources
8. `GET /v1/infrastructure/resources/containers` - Container resources
---
## Security & Access Control
All endpoints should:
- Log all requests (especially write operations)
- Support OIDC authentication when enabled
- Require admin privileges for destructive operations (remove, scale)
- Rate limit to prevent abuse
- Validate input parameters
- Return sanitized errors (no sensitive data in error messages)
---
## Error Handling
Standard error response format:
```json
{
"error": "ContainerNotFound",
"message": "Container 'nginx2' not found",
"details": {
"container": "nginx2",
"available_containers": ["nginx", "core-ai", "ollama"]
}
}
```
Common error codes:
- `400` - Bad Request (invalid parameters)
- `404` - Not Found (container/service doesn't exist)
- `409` - Conflict (invalid state transition)
- `500` - Internal Server Error (Portainer/Docker API failed)
- `503` - Service Unavailable (Portainer/Docker not accessible)
---
## Testing
Each endpoint should have:
- Unit tests (mock Portainer client)
- Integration tests (real Portainer/Docker)
- Error case tests (not found, permission denied, etc.)
- Performance tests (ensure response times < 2s)
---
## Questions / Decisions Needed
1. **Authentication**: Should container management require admin role, or allow read-only for all users?
2. **Rate Limiting**: What limits should be applied to prevent abuse?
3. **Caching**: Should container lists be cached? (TTL: 5s?)
4. **Async**: Should heavy operations (like logs) be async with job IDs?
5. **Webhooks**: Should operations emit events for monitoring?
---
## Notes
- All endpoints follow RESTful conventions
- Use existing PortainerClient methods where available
- Fall back to Docker socket when Portainer doesn't have data
- Log all operations with timestamps, user, and outcome
- Consider adding `/v1/infrastructure/containers/search` for fuzzy name matching
@@ -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