diff --git a/gns3server/schemas/controller/computes.py b/gns3server/schemas/controller/computes.py index 836ac9607..4924827e5 100644 --- a/gns3server/schemas/controller/computes.py +++ b/gns3server/schemas/controller/computes.py @@ -21,8 +21,11 @@ from typing import Any, List, Optional, Union from pydantic import BaseModel, ConfigDict, Field, SecretStr, model_validator from .base import DateTimeModelMixin -from .capabilities import UbridgeTcCapabilities -from .nodes import NodeType +# The capabilities payload has one definition (controller.capabilities): a +# second, private copy here silently stripped every field it did not know +# about from GET /computes/{id} — which is how ubridge_tap and +# ubridge_iol_tap went missing while the compute returned them. +from .capabilities import Capabilities class Protocol(str, Enum): @@ -93,22 +96,6 @@ class ComputeUpdate(ComputeBase): ) -class Capabilities(BaseModel): - """ - Capabilities supported by a compute. - """ - - version: str = Field(..., description="Compute version number") - node_types: List[NodeType] = Field(..., description="Node types supported by the compute") - platform: str = Field(..., description="Platform where the compute is running (Linux, Windows or macOS)") - cpus: int = Field(..., description="Number of CPUs on this compute") - memory: int = Field(..., description="Amount of memory on this compute") - disk_size: int = Field(..., description="Disk size on this compute") - ubridge_tc: Optional[UbridgeTcCapabilities] = Field( - None, description="uBridge tc-module capabilities; absent when the probe failed or uBridge has no tc module" - ) - - class Compute(DateTimeModelMixin, ComputeBase): """ Data returned for a compute. diff --git a/tests/api/routes/controller/test_computes.py b/tests/api/routes/controller/test_computes.py index 3c213c07c..83ced12b7 100644 --- a/tests/api/routes/controller/test_computes.py +++ b/tests/api/routes/controller/test_computes.py @@ -74,6 +74,40 @@ class TestComputeRoutes: assert response.status_code == status.HTTP_200_OK assert response.json()["compute_id"] == str(test_compute.compute_id) + async def test_compute_get_reports_every_capability(self, app: FastAPI, client: AsyncClient, controller) -> None: + """ + The capabilities payload the compute reports must survive the + response model intact. The local compute is the interesting one: it + is the controller object serialized straight through, and a second, + private Capabilities definition in the response schema silently + stripped the kernel-datapath fields (ubridge_tap, ubridge_iol_tap) + there — links were then built on a datapath clients could not see. + """ + + await controller.add_compute( + compute_id="local", name="local", host="127.0.0.1", port=3080, force=True, connect=False + ) + controller.get_compute("local")._capabilities.update( + { + "version": "3.1.0", + "node_types": ["docker"], + "platform": "linux", + "cpus": 4, + "memory": 1024, + "disk_size": 2048, + "ubridge_tc": {"netem": ["delay"], "ebpf": True, "ebpf_modes": ["nth"], "cbpf": True}, + "ubridge_tap": True, + "ubridge_iol_tap": False, + } + ) + + response = await client.get(app.url_path_for("get_compute", compute_id="local")) + assert response.status_code == status.HTTP_200_OK + capabilities = response.json()["capabilities"] + assert capabilities["ubridge_tap"] is True + assert capabilities["ubridge_iol_tap"] is False + assert capabilities["ubridge_tc"]["netem"] == ["delay"] + async def test_compute_get_local(self, app: FastAPI, client: AsyncClient, controller) -> None: await controller.add_compute(