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
@@ -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