devops.py
1 # Copyright 2026 Alibaba Group Holding Ltd. 2 # 3 # Licensed under the Apache License, Version 2.0 (the "License"); 4 # you may not use this file except in compliance with the License. 5 # You may obtain a copy of the License at 6 # 7 # http://www.apache.org/licenses/LICENSE-2.0 8 # 9 # Unless required by applicable law or agreed to in writing, software 10 # distributed under the License is distributed on an "AS IS" BASIS, 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 # See the License for the specific language governing permissions and 13 # limitations under the License. 14 15 """ 16 API routes for OpenSandbox DevOps diagnostics. 17 18 All endpoints return plain text for easy consumption by humans and AI agents. 19 """ 20 21 from typing import Optional 22 23 from fastapi import APIRouter, HTTPException, Query, status 24 from fastapi.responses import PlainTextResponse 25 26 from opensandbox_server.api.lifecycle import sandbox_service 27 28 router = APIRouter(tags=["DevOps"]) 29 30 31 @router.get( 32 "/sandboxes/{sandbox_id}/diagnostics/logs", 33 response_class=PlainTextResponse, 34 status_code=status.HTTP_200_OK, 35 responses={ 36 200: {"description": "Container logs as plain text", "content": {"text/plain": {}}}, 37 404: {"description": "Sandbox not found"}, 38 }, 39 ) 40 def get_sandbox_logs( 41 sandbox_id: str, 42 tail: int = Query(100, ge=1, le=10000, description="Number of trailing log lines"), 43 since: Optional[str] = Query(None, description="Only return logs newer than this duration (e.g. 10m, 1h)"), 44 ) -> PlainTextResponse: 45 """Retrieve container logs for a sandbox.""" 46 text = sandbox_service.get_sandbox_logs(sandbox_id, tail=tail, since=since) 47 return PlainTextResponse(content=text) 48 49 50 @router.get( 51 "/sandboxes/{sandbox_id}/diagnostics/inspect", 52 response_class=PlainTextResponse, 53 status_code=status.HTTP_200_OK, 54 responses={ 55 200: {"description": "Container inspection as plain text", "content": {"text/plain": {}}}, 56 404: {"description": "Sandbox not found"}, 57 }, 58 ) 59 def get_sandbox_inspect(sandbox_id: str) -> PlainTextResponse: 60 """Retrieve detailed inspection info for a sandbox container.""" 61 text = sandbox_service.get_sandbox_inspect(sandbox_id) 62 return PlainTextResponse(content=text) 63 64 65 @router.get( 66 "/sandboxes/{sandbox_id}/diagnostics/events", 67 response_class=PlainTextResponse, 68 status_code=status.HTTP_200_OK, 69 responses={ 70 200: {"description": "Sandbox events as plain text", "content": {"text/plain": {}}}, 71 404: {"description": "Sandbox not found"}, 72 }, 73 ) 74 def get_sandbox_events( 75 sandbox_id: str, 76 limit: int = Query(50, ge=1, le=500, description="Maximum number of events to return"), 77 ) -> PlainTextResponse: 78 """Retrieve events related to a sandbox.""" 79 text = sandbox_service.get_sandbox_events(sandbox_id, limit=limit) 80 return PlainTextResponse(content=text) 81 82 83 @router.get( 84 "/sandboxes/{sandbox_id}/diagnostics/summary", 85 response_class=PlainTextResponse, 86 status_code=status.HTTP_200_OK, 87 responses={ 88 200: {"description": "Combined diagnostics summary as plain text", "content": {"text/plain": {}}}, 89 404: {"description": "Sandbox not found"}, 90 }, 91 ) 92 def get_sandbox_diagnostics_summary( 93 sandbox_id: str, 94 tail: int = Query(50, ge=1, le=10000, description="Number of trailing log lines"), 95 event_limit: int = Query(20, ge=1, le=500, description="Maximum number of events"), 96 ) -> PlainTextResponse: 97 """One-shot diagnostics summary: inspect + events + logs.""" 98 sections: list[str] = [] 99 100 sections.append("=" * 72) 101 sections.append("SANDBOX DIAGNOSTICS SUMMARY") 102 sections.append(f"Sandbox ID: {sandbox_id}") 103 sections.append("=" * 72) 104 105 # Inspect — let HTTPException (e.g. 404) propagate so callers get a proper error 106 sections.append("") 107 sections.append("-" * 40) 108 sections.append("INSPECT") 109 sections.append("-" * 40) 110 try: 111 sections.append(sandbox_service.get_sandbox_inspect(sandbox_id)) 112 except HTTPException: 113 raise 114 except Exception as exc: 115 sections.append(f"[error] {exc}") 116 117 # Events 118 sections.append("") 119 sections.append("-" * 40) 120 sections.append("EVENTS") 121 sections.append("-" * 40) 122 try: 123 sections.append(sandbox_service.get_sandbox_events(sandbox_id, limit=event_limit)) 124 except HTTPException: 125 raise 126 except Exception as exc: 127 sections.append(f"[error] {exc}") 128 129 # Logs 130 sections.append("") 131 sections.append("-" * 40) 132 sections.append("LOGS (last {} lines)".format(tail)) 133 sections.append("-" * 40) 134 try: 135 sections.append(sandbox_service.get_sandbox_logs(sandbox_id, tail=tail)) 136 except HTTPException: 137 raise 138 except Exception as exc: 139 sections.append(f"[error] {exc}") 140 141 return PlainTextResponse(content="\n".join(sections) + "\n")