/ server / opensandbox_server / api / devops.py
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")