mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-10-22 19:02:23 +03:00
fix: accept the 'local' compute id in compute tools
The compute_get/compute_images MCP tools typed compute_id as a UUID, so passing 'local' (the actual id of the built-in compute, which the compute_images description itself pointed to) was rejected by schema validation. Both tools now take a string defaulting to 'local', and the compute_get REST route resolves 'local' through the controller since the local compute has no database entry.
This commit is contained in:
parent
6703e50487
commit
df25e037ea
@ -31,7 +31,6 @@ import json
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import socket
|
import socket
|
||||||
import uuid
|
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
import bcrypt
|
import bcrypt
|
||||||
from typing import Any, Annotated
|
from typing import Any, Annotated
|
||||||
@ -772,12 +771,12 @@ async def compute_list() -> list[dict[str, Any]]:
|
|||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def compute_get(
|
async def compute_get(
|
||||||
compute_id: Annotated[uuid.UUID, Field(description="Compute UUID from compute_list output")],
|
compute_id: Annotated[str, Field(description="Compute ID: 'local' (default) for the built-in local compute, or a compute UUID from compute_list")] = "local",
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
"""Get detailed information about a registered remote compute node.
|
"""Get detailed information about a compute node.
|
||||||
|
|
||||||
NOTE: Only works for computes registered in the database (returned by compute_list).
|
Accepts 'local' for the built-in local compute or a UUID from compute_list
|
||||||
For the built-in local compute info, use server_statistics instead.
|
for a registered remote compute.
|
||||||
"""
|
"""
|
||||||
return await asyncio.to_thread(_run_handler_sync, get_compute_handler, {"compute_id": compute_id})
|
return await asyncio.to_thread(_run_handler_sync, get_compute_handler, {"compute_id": compute_id})
|
||||||
|
|
||||||
@ -785,12 +784,12 @@ async def compute_get(
|
|||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def compute_images(
|
async def compute_images(
|
||||||
emulator: Annotated[str, Field(description="Emulator type (e.g. qemu, iou, docker)")],
|
emulator: Annotated[str, Field(description="Emulator type (e.g. qemu, iou, docker)")],
|
||||||
compute_id: Annotated[uuid.UUID, Field(description="Compute UUID from compute_list output")],
|
compute_id: Annotated[str, Field(description="Compute ID: 'local' (default) for the built-in local compute, or a compute UUID from compute_list")] = "local",
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
"""List available images for an emulator on a registered compute node.
|
"""List available images for an emulator on a compute node.
|
||||||
|
|
||||||
NOTE: Only works for computes registered in the database.
|
Accepts 'local' for the built-in local compute or a UUID from compute_list
|
||||||
For the local compute, the default compute_id is typically found via server_statistics.
|
for a registered remote compute.
|
||||||
"""
|
"""
|
||||||
return await asyncio.to_thread(_run_handler_sync, get_compute_images_handler, {
|
return await asyncio.to_thread(_run_handler_sync, get_compute_images_handler, {
|
||||||
"emulator": emulator, "compute_id": compute_id,
|
"emulator": emulator, "compute_id": compute_id,
|
||||||
|
|||||||
@ -42,20 +42,16 @@ def list_computes_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> d
|
|||||||
|
|
||||||
|
|
||||||
def get_compute_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any]:
|
def get_compute_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any]:
|
||||||
compute_id = params.get("compute_id")
|
compute_id = params.get("compute_id") or "local"
|
||||||
if not compute_id:
|
|
||||||
return {"error": "compute_id is required (use compute_list to get the UUID)"}
|
|
||||||
conn = _get_connector(gns3_ctx)
|
conn = _get_connector(gns3_ctx)
|
||||||
return conn.http_call("get", f"{conn.base_url}/computes/{compute_id}").json()
|
return conn.http_call("get", f"{conn.base_url}/computes/{compute_id}").json()
|
||||||
|
|
||||||
|
|
||||||
def get_compute_images_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any]:
|
def get_compute_images_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any]:
|
||||||
emulator = params.get("emulator")
|
emulator = params.get("emulator")
|
||||||
compute_id = params.get("compute_id")
|
compute_id = params.get("compute_id") or "local"
|
||||||
if not emulator:
|
if not emulator:
|
||||||
return {"error": "emulator is required (e.g. qemu, iou, docker)"}
|
return {"error": "emulator is required (e.g. qemu, iou, docker)"}
|
||||||
if not compute_id:
|
|
||||||
return {"error": "compute_id is required (use compute_list to get the UUID)"}
|
|
||||||
conn = _get_connector(gns3_ctx)
|
conn = _get_connector(gns3_ctx)
|
||||||
images = conn.http_call("get", f"{conn.base_url}/computes/{compute_id}/{emulator}/images").json()
|
images = conn.http_call("get", f"{conn.base_url}/computes/{compute_id}/{emulator}/images").json()
|
||||||
return {"images": images, "count": len(images)}
|
return {"images": images, "count": len(images)}
|
||||||
|
|||||||
@ -54,8 +54,12 @@ class ComputesService:
|
|||||||
self._controller.notification.controller_emit("compute.created", compute.asdict())
|
self._controller.notification.controller_emit("compute.created", compute.asdict())
|
||||||
return db_compute
|
return db_compute
|
||||||
|
|
||||||
async def get_compute(self, compute_id: Union[str, UUID]) -> models.Compute:
|
async def get_compute(self, compute_id: Union[str, UUID]) -> Union[models.Compute, dict]:
|
||||||
|
|
||||||
|
if str(compute_id) == "local":
|
||||||
|
# the built-in local compute only lives in the controller, not in the database;
|
||||||
|
# drop unset fields (e.g. user) as the response schema types them as str
|
||||||
|
return {k: v for k, v in self._controller.get_compute("local").asdict().items() if v is not None}
|
||||||
db_compute = await self._computes_repo.get_compute(compute_id)
|
db_compute = await self._computes_repo.get_compute(compute_id)
|
||||||
if not db_compute:
|
if not db_compute:
|
||||||
raise ControllerNotFoundError(f"Compute '{compute_id}' not found")
|
raise ControllerNotFoundError(f"Compute '{compute_id}' not found")
|
||||||
|
|||||||
@ -553,6 +553,44 @@ class TestImage:
|
|||||||
assert result == {"message": "Image installation completed"}
|
assert result == {"message": "Image installation completed"}
|
||||||
|
|
||||||
|
|
||||||
|
class TestCompute:
|
||||||
|
|
||||||
|
mod = "computes"
|
||||||
|
|
||||||
|
def test_get_local_by_default(self, ctx):
|
||||||
|
from gns3server.agent.mcp.computes import get_compute_handler
|
||||||
|
with patch(f"{BASE}.{self.mod}._get_connector") as m:
|
||||||
|
conn = _mock_conn({"compute_id": "local", "name": "local"})
|
||||||
|
m.return_value = conn
|
||||||
|
result = get_compute_handler({}, ctx)
|
||||||
|
assert result["compute_id"] == "local"
|
||||||
|
url = conn.http_call.call_args[0][1]
|
||||||
|
assert url.endswith("/computes/local")
|
||||||
|
|
||||||
|
def test_get_explicit_compute_id(self, ctx):
|
||||||
|
from gns3server.agent.mcp.computes import get_compute_handler
|
||||||
|
with patch(f"{BASE}.{self.mod}._get_connector") as m:
|
||||||
|
conn = _mock_conn({"compute_id": "4fcfb6b5-5b0b-4f43-bd5e-e8ae2a69c8e6"})
|
||||||
|
m.return_value = conn
|
||||||
|
get_compute_handler({"compute_id": "4fcfb6b5-5b0b-4f43-bd5e-e8ae2a69c8e6"}, ctx)
|
||||||
|
url = conn.http_call.call_args[0][1]
|
||||||
|
assert url.endswith("/computes/4fcfb6b5-5b0b-4f43-bd5e-e8ae2a69c8e6")
|
||||||
|
|
||||||
|
def test_images_local_by_default(self, ctx):
|
||||||
|
from gns3server.agent.mcp.computes import get_compute_images_handler
|
||||||
|
with patch(f"{BASE}.{self.mod}._get_connector") as m:
|
||||||
|
conn = _mock_conn(["img1.qcow2"])
|
||||||
|
m.return_value = conn
|
||||||
|
result = get_compute_images_handler({"emulator": "qemu"}, ctx)
|
||||||
|
assert result["count"] == 1
|
||||||
|
url = conn.http_call.call_args[0][1]
|
||||||
|
assert url.endswith("/computes/local/qemu/images")
|
||||||
|
|
||||||
|
def test_images_requires_emulator(self, ctx):
|
||||||
|
from gns3server.agent.mcp.computes import get_compute_images_handler
|
||||||
|
assert "error" in get_compute_images_handler({}, ctx)
|
||||||
|
|
||||||
|
|
||||||
# ── Marker (traffic-insight) ────────────────────────────────────────────
|
# ── Marker (traffic-insight) ────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -79,6 +79,16 @@ class TestComputeRoutes:
|
|||||||
assert response.status_code == status.HTTP_200_OK
|
assert response.status_code == status.HTTP_200_OK
|
||||||
assert response.json()["compute_id"] == str(test_compute.compute_id)
|
assert response.json()["compute_id"] == str(test_compute.compute_id)
|
||||||
|
|
||||||
|
async def test_compute_get_local(self, app: FastAPI, client: AsyncClient, controller) -> None:
|
||||||
|
|
||||||
|
await controller.add_compute(
|
||||||
|
compute_id="local", name="local", host="127.0.0.1", port=3080, force=True, connect=False)
|
||||||
|
|
||||||
|
response = await client.get(app.url_path_for("get_compute", compute_id="local"))
|
||||||
|
assert response.status_code == status.HTTP_200_OK
|
||||||
|
assert response.json()["compute_id"] == "local"
|
||||||
|
assert response.json()["name"] == "local"
|
||||||
|
|
||||||
async def test_compute_update(self, app: FastAPI, client: AsyncClient, test_compute: Compute) -> None:
|
async def test_compute_update(self, app: FastAPI, client: AsyncClient, test_compute: Compute) -> None:
|
||||||
|
|
||||||
params = {
|
params = {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user